feat: add Reachability Center and Why Drawer components with tests
- Implemented ReachabilityCenterComponent for displaying asset reachability status with summary and filtering options. - Added ReachabilityWhyDrawerComponent to show detailed reachability evidence and call paths. - Created unit tests for both components to ensure functionality and correctness. - Updated accessibility test results for the new components.
This commit is contained in:
@@ -1,187 +1,165 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Connector.Acsc;
|
||||
using StellaOps.Concelier.Connector.Acsc.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.Acsc;
|
||||
using StellaOps.Concelier.Connector.Acsc.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Acsc.Tests.Acsc;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class AcscConnectorFetchTests : IAsyncLifetime
|
||||
{
|
||||
private static readonly Uri BaseEndpoint = new("https://origin.example/");
|
||||
private static readonly Uri RelayEndpoint = new("https://relay.example/");
|
||||
private static readonly Uri AlertsDirectUri = new(BaseEndpoint, "/feeds/alerts/rss");
|
||||
private static readonly Uri AlertsRelayUri = new(RelayEndpoint, "/feeds/alerts/rss");
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
|
||||
public AcscConnectorFetchTests(MongoIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero));
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchAsync_DirectSuccessAdvancesCursor()
|
||||
{
|
||||
await using var provider = await BuildProviderAsync(preferRelay: false);
|
||||
|
||||
var connector = provider.GetRequiredService<AcscConnector>();
|
||||
SeedRssResponse(AlertsDirectUri, "direct", DateTimeOffset.Parse("2025-10-10T02:15:00Z"), DateTimeOffset.Parse("2025-10-11T05:30:00Z"));
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
_handler.AssertNoPendingResponses();
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.Equal("Direct", state!.Cursor.GetValue("preferredEndpoint").AsString);
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class AcscConnectorFetchTests
|
||||
{
|
||||
private static readonly Uri BaseEndpoint = new("https://origin.example/");
|
||||
private static readonly Uri RelayEndpoint = new("https://relay.example/");
|
||||
private static readonly Uri AlertsDirectUri = new(BaseEndpoint, "/feeds/alerts/rss");
|
||||
private static readonly Uri AlertsRelayUri = new(RelayEndpoint, "/feeds/alerts/rss");
|
||||
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
|
||||
public AcscConnectorFetchTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchAsync_DirectSuccessAdvancesCursor()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync(preferRelay: false);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<AcscConnector>();
|
||||
SeedRssResponse(
|
||||
harness.Handler,
|
||||
AlertsDirectUri,
|
||||
"direct",
|
||||
DateTimeOffset.Parse("2025-10-10T02:15:00Z"),
|
||||
DateTimeOffset.Parse("2025-10-11T05:30:00Z"));
|
||||
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
harness.Handler.AssertNoPendingResponses();
|
||||
|
||||
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 feeds = state.Cursor.GetValue("feeds").AsBsonDocument;
|
||||
Assert.True(feeds.TryGetValue("alerts", out var published));
|
||||
Assert.Equal(DateTime.Parse("2025-10-11T05:30:00Z").ToUniversalTime(), published.ToUniversalTime());
|
||||
|
||||
var pendingDocuments = state.Cursor.GetValue("pendingDocuments").AsBsonArray;
|
||||
Assert.Single(pendingDocuments);
|
||||
|
||||
var documentStore = provider.GetRequiredService<IDocumentStore>();
|
||||
var documentId = Guid.Parse(pendingDocuments[0]!.AsString);
|
||||
var document = await documentStore.FindAsync(documentId, CancellationToken.None);
|
||||
Assert.NotNull(document);
|
||||
var pendingDocuments = state.Cursor.GetValue("pendingDocuments").AsBsonArray;
|
||||
Assert.Single(pendingDocuments);
|
||||
|
||||
var documentStore = harness.ServiceProvider.GetRequiredService<IDocumentStore>();
|
||||
var documentId = Guid.Parse(pendingDocuments[0]!.AsString);
|
||||
var document = await documentStore.FindAsync(documentId, CancellationToken.None);
|
||||
Assert.NotNull(document);
|
||||
Assert.Equal(DocumentStatuses.PendingParse, document!.Status);
|
||||
var directMetadata = document.Metadata ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
Assert.True(directMetadata.TryGetValue("acsc.fetch.mode", out var mode));
|
||||
Assert.Equal("direct", mode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchAsync_DirectFailureFallsBackToRelay()
|
||||
{
|
||||
await using var provider = await BuildProviderAsync(preferRelay: false);
|
||||
|
||||
var connector = provider.GetRequiredService<AcscConnector>();
|
||||
_handler.AddException(HttpMethod.Get, AlertsDirectUri, new HttpRequestException("HTTP/2 reset"));
|
||||
SeedRssResponse(AlertsRelayUri, "relay", DateTimeOffset.Parse("2025-10-09T10:00:00Z"), DateTimeOffset.Parse("2025-10-11T00:00:00Z"));
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
_handler.AssertNoPendingResponses();
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.Equal("Relay", state!.Cursor.GetValue("preferredEndpoint").AsString);
|
||||
[Fact]
|
||||
public async Task FetchAsync_DirectFailureFallsBackToRelay()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync(preferRelay: false);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<AcscConnector>();
|
||||
harness.Handler.AddException(HttpMethod.Get, AlertsDirectUri, new HttpRequestException("HTTP/2 reset"));
|
||||
SeedRssResponse(
|
||||
harness.Handler,
|
||||
AlertsRelayUri,
|
||||
"relay",
|
||||
DateTimeOffset.Parse("2025-10-09T10:00:00Z"),
|
||||
DateTimeOffset.Parse("2025-10-11T00:00:00Z"));
|
||||
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
harness.Handler.AssertNoPendingResponses();
|
||||
|
||||
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 feeds = state.Cursor.GetValue("feeds").AsBsonDocument;
|
||||
Assert.True(feeds.TryGetValue("alerts", out var published));
|
||||
Assert.Equal(DateTime.Parse("2025-10-11T00:00:00Z").ToUniversalTime(), published.ToUniversalTime());
|
||||
|
||||
var pendingDocuments = state.Cursor.GetValue("pendingDocuments").AsBsonArray;
|
||||
Assert.Single(pendingDocuments);
|
||||
|
||||
var documentStore = provider.GetRequiredService<IDocumentStore>();
|
||||
var documentId = Guid.Parse(pendingDocuments[0]!.AsString);
|
||||
var document = await documentStore.FindAsync(documentId, CancellationToken.None);
|
||||
Assert.NotNull(document);
|
||||
var pendingDocuments = state.Cursor.GetValue("pendingDocuments").AsBsonArray;
|
||||
Assert.Single(pendingDocuments);
|
||||
|
||||
var documentStore = harness.ServiceProvider.GetRequiredService<IDocumentStore>();
|
||||
var documentId = Guid.Parse(pendingDocuments[0]!.AsString);
|
||||
var document = await documentStore.FindAsync(documentId, CancellationToken.None);
|
||||
Assert.NotNull(document);
|
||||
Assert.Equal(DocumentStatuses.PendingParse, document!.Status);
|
||||
var metadata = document.Metadata ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
Assert.True(metadata.TryGetValue("acsc.fetch.mode", out var mode));
|
||||
Assert.Equal("relay", mode);
|
||||
|
||||
Assert.Collection(_handler.Requests,
|
||||
request =>
|
||||
{
|
||||
Assert.Equal(HttpMethod.Get, request.Method);
|
||||
Assert.Equal(AlertsDirectUri, request.Uri);
|
||||
Assert.True(metadata.TryGetValue("acsc.fetch.mode", out var mode));
|
||||
Assert.Equal("relay", mode);
|
||||
|
||||
Assert.Collection(harness.Handler.Requests,
|
||||
request =>
|
||||
{
|
||||
Assert.Equal(HttpMethod.Get, request.Method);
|
||||
Assert.Equal(AlertsDirectUri, request.Uri);
|
||||
},
|
||||
request =>
|
||||
{
|
||||
Assert.Equal(HttpMethod.Get, request.Method);
|
||||
Assert.Equal(AlertsRelayUri, request.Uri);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task InitializeAsync() => await Task.CompletedTask;
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
private async Task<ServiceProvider> BuildProviderAsync(bool preferRelay)
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddAcscConnector(options =>
|
||||
{
|
||||
options.BaseEndpoint = BaseEndpoint;
|
||||
options.RelayEndpoint = RelayEndpoint;
|
||||
options.EnableRelayFallback = true;
|
||||
options.PreferRelayByDefault = preferRelay;
|
||||
options.ForceRelay = false;
|
||||
options.RequestTimeout = TimeSpan.FromSeconds(10);
|
||||
options.Feeds.Clear();
|
||||
options.Feeds.Add(new AcscFeedOptions
|
||||
{
|
||||
Slug = "alerts",
|
||||
RelativePath = "/feeds/alerts/rss",
|
||||
Enabled = true,
|
||||
});
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(AcscOptions.HttpClientName, builderOptions =>
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler);
|
||||
});
|
||||
services.Configure<SourceHttpClientOptions>(AcscOptions.HttpClientName, options =>
|
||||
{
|
||||
options.MaxAttempts = 1;
|
||||
options.BaseDelay = TimeSpan.Zero;
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private void SeedRssResponse(Uri uri, string mode, DateTimeOffset first, DateTimeOffset second)
|
||||
{
|
||||
var payload = CreateRssPayload(first, second);
|
||||
_handler.AddResponse(HttpMethod.Get, uri, _ =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"),
|
||||
Assert.Equal(AlertsRelayUri, request.Uri);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<ConnectorTestHarness> BuildHarnessAsync(bool preferRelay)
|
||||
{
|
||||
var initialTime = new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero);
|
||||
var harness = new ConnectorTestHarness(_fixture, initialTime, AcscOptions.HttpClientName);
|
||||
await harness.EnsureServiceProviderAsync(services =>
|
||||
{
|
||||
services.AddAcscConnector(options =>
|
||||
{
|
||||
options.BaseEndpoint = BaseEndpoint;
|
||||
options.RelayEndpoint = RelayEndpoint;
|
||||
options.EnableRelayFallback = true;
|
||||
options.PreferRelayByDefault = preferRelay;
|
||||
options.ForceRelay = false;
|
||||
options.RequestTimeout = TimeSpan.FromSeconds(10);
|
||||
options.Feeds.Clear();
|
||||
options.Feeds.Add(new AcscFeedOptions
|
||||
{
|
||||
Slug = "alerts",
|
||||
RelativePath = "/feeds/alerts/rss",
|
||||
Enabled = true,
|
||||
});
|
||||
});
|
||||
|
||||
services.Configure<SourceHttpClientOptions>(AcscOptions.HttpClientName, options =>
|
||||
{
|
||||
options.MaxAttempts = 1;
|
||||
options.BaseDelay = TimeSpan.Zero;
|
||||
});
|
||||
});
|
||||
return harness;
|
||||
}
|
||||
|
||||
private static void SeedRssResponse(CannedHttpMessageHandler handler, Uri uri, string mode, DateTimeOffset first, DateTimeOffset second)
|
||||
{
|
||||
var payload = CreateRssPayload(first, second);
|
||||
handler.AddResponse(HttpMethod.Get, uri, _ =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"),
|
||||
};
|
||||
|
||||
response.Headers.ETag = new System.Net.Http.Headers.EntityTagHeaderValue($"\"{mode}-etag\"");
|
||||
|
||||
@@ -1,88 +1,78 @@
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Acsc;
|
||||
using StellaOps.Concelier.Connector.Acsc.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Acsc;
|
||||
using StellaOps.Concelier.Connector.Acsc.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Acsc.Tests.Acsc;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class AcscConnectorParseTests : IAsyncLifetime
|
||||
{
|
||||
private static readonly Uri BaseEndpoint = new("https://origin.example/");
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class AcscConnectorParseTests
|
||||
{
|
||||
private static readonly Uri BaseEndpoint = new("https://origin.example/");
|
||||
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
|
||||
public AcscConnectorParseTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
|
||||
public AcscConnectorParseTests(MongoIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero));
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_PersistsDtoAndAdvancesCursor()
|
||||
{
|
||||
await using var provider = await BuildProviderAsync();
|
||||
var connector = provider.GetRequiredService<AcscConnector>();
|
||||
|
||||
var feedUri = new Uri(BaseEndpoint, "/feeds/alerts/rss");
|
||||
SeedRssResponse(feedUri);
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
_handler.AssertNoPendingResponses();
|
||||
|
||||
var documentStore = provider.GetRequiredService<IDocumentStore>();
|
||||
var document = await documentStore.FindBySourceAndUriAsync(AcscConnectorPlugin.SourceName, feedUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(document);
|
||||
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
|
||||
var refreshed = await documentStore.FindAsync(document!.Id, CancellationToken.None);
|
||||
Assert.NotNull(refreshed);
|
||||
Assert.Equal(DocumentStatuses.PendingMap, refreshed!.Status);
|
||||
|
||||
var dtoStore = provider.GetRequiredService<IDtoStore>();
|
||||
var dtoRecord = await dtoStore.FindByDocumentIdAsync(document.Id, CancellationToken.None);
|
||||
Assert.NotNull(dtoRecord);
|
||||
Assert.Equal("acsc.feed.v1", dtoRecord!.SchemaVersion);
|
||||
[Fact]
|
||||
public async Task ParseAsync_PersistsDtoAndAdvancesCursor()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
var connector = harness.ServiceProvider.GetRequiredService<AcscConnector>();
|
||||
|
||||
var feedUri = new Uri(BaseEndpoint, "/feeds/alerts/rss");
|
||||
SeedRssResponse(harness.Handler, feedUri);
|
||||
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
harness.Handler.AssertNoPendingResponses();
|
||||
|
||||
var documentStore = harness.ServiceProvider.GetRequiredService<IDocumentStore>();
|
||||
var document = await documentStore.FindBySourceAndUriAsync(AcscConnectorPlugin.SourceName, feedUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(document);
|
||||
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var refreshed = await documentStore.FindAsync(document!.Id, CancellationToken.None);
|
||||
Assert.NotNull(refreshed);
|
||||
Assert.Equal(DocumentStatuses.PendingMap, refreshed!.Status);
|
||||
|
||||
var dtoStore = harness.ServiceProvider.GetRequiredService<IDtoStore>();
|
||||
var dtoRecord = await dtoStore.FindByDocumentIdAsync(document.Id, CancellationToken.None);
|
||||
Assert.NotNull(dtoRecord);
|
||||
Assert.Equal("acsc.feed.v1", dtoRecord!.SchemaVersion);
|
||||
|
||||
var payload = dtoRecord.Payload;
|
||||
Assert.NotNull(payload);
|
||||
Assert.Equal("alerts", payload.GetValue("feedSlug").AsString);
|
||||
Assert.Single(payload.GetValue("entries").AsBsonArray);
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.DoesNotContain(document.Id.ToString(), state!.Cursor.GetValue("pendingDocuments").AsBsonArray.Select(v => v.AsString));
|
||||
Assert.Contains(document.Id.ToString(), state.Cursor.GetValue("pendingMappings").AsBsonArray.Select(v => v.AsString));
|
||||
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoriesStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoriesStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Single(advisories);
|
||||
Assert.Equal("alerts", payload.GetValue("feedSlug").AsString);
|
||||
Assert.Single(payload.GetValue("entries").AsBsonArray);
|
||||
|
||||
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").AsBsonArray.Select(v => v.AsString));
|
||||
Assert.Contains(document.Id.ToString(), state.Cursor.GetValue("pendingMappings").AsBsonArray.Select(v => v.AsString));
|
||||
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var advisoriesStore = harness.ServiceProvider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoriesStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Single(advisories);
|
||||
|
||||
var ordered = advisories
|
||||
.OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.OrdinalIgnoreCase)
|
||||
@@ -101,46 +91,46 @@ public sealed class AcscConnectorParseTests : IAsyncLifetime
|
||||
Assert.True(state!.Cursor.GetValue("pendingMappings").AsBsonArray.Count == 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAsync_MultiEntryFeedProducesExpectedSnapshot()
|
||||
{
|
||||
await using var provider = await BuildProviderAsync(options =>
|
||||
{
|
||||
options.Feeds.Clear();
|
||||
options.Feeds.Add(new AcscFeedOptions
|
||||
{
|
||||
Slug = "multi",
|
||||
RelativePath = "/feeds/multi/rss",
|
||||
Enabled = true,
|
||||
});
|
||||
});
|
||||
var connector = provider.GetRequiredService<AcscConnector>();
|
||||
|
||||
var feedUri = new Uri(BaseEndpoint, "/feeds/multi/rss");
|
||||
SeedMultiEntryResponse(feedUri);
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
|
||||
var documentStore = provider.GetRequiredService<IDocumentStore>();
|
||||
var dtoStore = provider.GetRequiredService<IDtoStore>();
|
||||
var document = await documentStore.FindBySourceAndUriAsync(AcscConnectorPlugin.SourceName, feedUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(document);
|
||||
var dtoRecord = await dtoStore.FindByDocumentIdAsync(document!.Id, CancellationToken.None);
|
||||
[Fact]
|
||||
public async Task MapAsync_MultiEntryFeedProducesExpectedSnapshot()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync(options =>
|
||||
{
|
||||
options.Feeds.Clear();
|
||||
options.Feeds.Add(new AcscFeedOptions
|
||||
{
|
||||
Slug = "multi",
|
||||
RelativePath = "/feeds/multi/rss",
|
||||
Enabled = true,
|
||||
});
|
||||
});
|
||||
var connector = harness.ServiceProvider.GetRequiredService<AcscConnector>();
|
||||
|
||||
var feedUri = new Uri(BaseEndpoint, "/feeds/multi/rss");
|
||||
SeedMultiEntryResponse(harness.Handler, feedUri);
|
||||
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var documentStore = harness.ServiceProvider.GetRequiredService<IDocumentStore>();
|
||||
var dtoStore = harness.ServiceProvider.GetRequiredService<IDtoStore>();
|
||||
var document = await documentStore.FindBySourceAndUriAsync(AcscConnectorPlugin.SourceName, feedUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(document);
|
||||
var dtoRecord = await dtoStore.FindByDocumentIdAsync(document!.Id, CancellationToken.None);
|
||||
Assert.NotNull(dtoRecord);
|
||||
var payload = dtoRecord!.Payload;
|
||||
Assert.NotNull(payload);
|
||||
var entries = payload.GetValue("entries").AsBsonArray;
|
||||
Assert.Equal(2, entries.Count);
|
||||
var fields = entries[0].AsBsonDocument.GetValue("fields").AsBsonDocument;
|
||||
Assert.Equal("Critical", fields.GetValue("severity").AsString);
|
||||
Assert.Equal("ExampleCo Router X, ExampleCo Router Y", fields.GetValue("systemsAffected").AsString);
|
||||
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
Assert.Equal("Critical", fields.GetValue("severity").AsString);
|
||||
Assert.Equal("ExampleCo Router X, ExampleCo Router Y", fields.GetValue("systemsAffected").AsString);
|
||||
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = harness.ServiceProvider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
|
||||
var ordered = advisories
|
||||
.OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.OrdinalIgnoreCase)
|
||||
@@ -155,67 +145,44 @@ public sealed class AcscConnectorParseTests : IAsyncLifetime
|
||||
Assert.Equal("critical", ordered.First(a => a.Severity is not null).Severity, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
private async Task<ConnectorTestHarness> BuildHarnessAsync(Action<AcscOptions>? configure = null)
|
||||
{
|
||||
var initialTime = new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero);
|
||||
var harness = new ConnectorTestHarness(_fixture, initialTime, AcscOptions.HttpClientName);
|
||||
await harness.EnsureServiceProviderAsync(services =>
|
||||
{
|
||||
services.AddAcscConnector(options =>
|
||||
{
|
||||
options.BaseEndpoint = BaseEndpoint;
|
||||
options.RelayEndpoint = null;
|
||||
options.PreferRelayByDefault = false;
|
||||
options.ForceRelay = false;
|
||||
options.EnableRelayFallback = false;
|
||||
options.RequestTimeout = TimeSpan.FromSeconds(10);
|
||||
options.Feeds.Clear();
|
||||
options.Feeds.Add(new AcscFeedOptions
|
||||
{
|
||||
Slug = "alerts",
|
||||
RelativePath = "/feeds/alerts/rss",
|
||||
Enabled = true,
|
||||
});
|
||||
configure?.Invoke(options);
|
||||
});
|
||||
|
||||
services.Configure<SourceHttpClientOptions>(AcscOptions.HttpClientName, options =>
|
||||
{
|
||||
options.MaxAttempts = 1;
|
||||
options.BaseDelay = TimeSpan.Zero;
|
||||
});
|
||||
});
|
||||
return harness;
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
private async Task<ServiceProvider> BuildProviderAsync(Action<AcscOptions>? configure = null)
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddAcscConnector(options =>
|
||||
{
|
||||
options.BaseEndpoint = BaseEndpoint;
|
||||
options.RelayEndpoint = null;
|
||||
options.PreferRelayByDefault = false;
|
||||
options.ForceRelay = false;
|
||||
options.EnableRelayFallback = false;
|
||||
options.RequestTimeout = TimeSpan.FromSeconds(10);
|
||||
options.Feeds.Clear();
|
||||
options.Feeds.Add(new AcscFeedOptions
|
||||
{
|
||||
Slug = "alerts",
|
||||
RelativePath = "/feeds/alerts/rss",
|
||||
Enabled = true,
|
||||
});
|
||||
configure?.Invoke(options);
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(AcscOptions.HttpClientName, builderOptions =>
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler);
|
||||
});
|
||||
|
||||
services.Configure<SourceHttpClientOptions>(AcscOptions.HttpClientName, options =>
|
||||
{
|
||||
options.MaxAttempts = 1;
|
||||
options.BaseDelay = TimeSpan.Zero;
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private void SeedRssResponse(Uri uri)
|
||||
{
|
||||
const string payload = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||
private static void SeedRssResponse(CannedHttpMessageHandler handler, Uri uri)
|
||||
{
|
||||
const string payload = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||
<channel>
|
||||
<title>ACSC Alerts</title>
|
||||
<link>https://origin.example/feeds/alerts</link>
|
||||
@@ -233,26 +200,26 @@ public sealed class AcscConnectorParseTests : IAsyncLifetime
|
||||
]]></content:encoded>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
""";
|
||||
|
||||
_handler.AddResponse(HttpMethod.Get, uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"),
|
||||
</rss>
|
||||
""";
|
||||
|
||||
handler.AddResponse(HttpMethod.Get, uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"),
|
||||
};
|
||||
response.Headers.ETag = new EntityTagHeaderValue("\"parse-etag\"");
|
||||
response.Content.Headers.LastModified = new DateTimeOffset(2025, 10, 12, 4, 20, 0, TimeSpan.Zero);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private void SeedMultiEntryResponse(Uri uri)
|
||||
{
|
||||
const string payload = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private static void SeedMultiEntryResponse(CannedHttpMessageHandler handler, Uri uri)
|
||||
{
|
||||
const string payload = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||
<channel>
|
||||
<title>ACSC Advisories</title>
|
||||
<link>https://origin.example/feeds/advisories</link>
|
||||
@@ -282,14 +249,14 @@ public sealed class AcscConnectorParseTests : IAsyncLifetime
|
||||
]]></content:encoded>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
""";
|
||||
|
||||
_handler.AddResponse(HttpMethod.Get, uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"),
|
||||
</rss>
|
||||
""";
|
||||
|
||||
handler.AddResponse(HttpMethod.Get, uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(payload, Encoding.UTF8, "application/rss+xml"),
|
||||
};
|
||||
response.Headers.ETag = new EntityTagHeaderValue("\"multi-etag\"");
|
||||
response.Content.Headers.LastModified = new DateTimeOffset(2025, 10, 12, 5, 0, 0, TimeSpan.Zero);
|
||||
|
||||
@@ -1,59 +1,50 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Connector.Cccs;
|
||||
using StellaOps.Concelier.Connector.Cccs.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.Cccs;
|
||||
using StellaOps.Concelier.Connector.Cccs.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Cccs.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class CccsConnectorTests : IAsyncLifetime
|
||||
{
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class CccsConnectorTests
|
||||
{
|
||||
private static readonly Uri FeedUri = new("https://test.local/api/cccs/threats/v1/get?lang=en&content_type=cccs_threat");
|
||||
private static readonly Uri TaxonomyUri = new("https://test.local/api/cccs/taxonomy/v1/get?lang=en&vocabulary=cccs_alert_type");
|
||||
private static readonly Uri TaxonomyUri = new("https://test.local/api/cccs/taxonomy/v1/get?lang=en&vocabulary=cccs_alert_type");
|
||||
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
|
||||
public CccsConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
|
||||
public CccsConnectorTests(MongoIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesCanonicalAdvisory()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedFeedResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<CccsConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
advisories.Should().HaveCount(1);
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesCanonicalAdvisory()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
SeedFeedResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<CccsConnector>();
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = harness.ServiceProvider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
advisories.Should().HaveCount(1);
|
||||
|
||||
var advisory = advisories[0];
|
||||
advisory.AdvisoryKey.Should().Be("TEST-001");
|
||||
@@ -64,9 +55,9 @@ public sealed class CccsConnectorTests : IAsyncLifetime
|
||||
advisory.AffectedPackages.Should().ContainSingle(pkg => pkg.Identifier == "Vendor Widget 1.0");
|
||||
advisory.AffectedPackages.Should().Contain(pkg => pkg.Identifier == "Vendor Widget 2.0");
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(CccsConnectorPlugin.SourceName, CancellationToken.None);
|
||||
state.Should().NotBeNull();
|
||||
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(CccsConnectorPlugin.SourceName, CancellationToken.None);
|
||||
state.Should().NotBeNull();
|
||||
state!.Cursor.Should().NotBeNull();
|
||||
state.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue();
|
||||
pendingDocs!.AsBsonArray.Should().BeEmpty();
|
||||
@@ -74,99 +65,65 @@ public sealed class CccsConnectorTests : IAsyncLifetime
|
||||
pendingMappings!.AsBsonArray.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Fetch_PersistsRawDocumentWithMetadata()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedFeedResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<CccsConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
|
||||
var mongo = provider.GetRequiredService<IMongoDatabase>();
|
||||
var docCollection = mongo.GetCollection<BsonDocument>("document");
|
||||
var documentsSnapshot = await docCollection.Find(FilterDefinition<BsonDocument>.Empty).ToListAsync();
|
||||
|
||||
System.IO.Directory.CreateDirectory(System.IO.Path.Combine(AppContext.BaseDirectory, "tmp"));
|
||||
var debugPath = System.IO.Path.Combine(AppContext.BaseDirectory, "tmp", "cccs-documents.json");
|
||||
await System.IO.File.WriteAllTextAsync(debugPath, documentsSnapshot.ToJson(new MongoDB.Bson.IO.JsonWriterSettings { Indent = true }));
|
||||
|
||||
var documentStore = provider.GetRequiredService<IDocumentStore>();
|
||||
var document = await documentStore.FindBySourceAndUriAsync(CccsConnectorPlugin.SourceName, "https://www.cyber.gc.ca/en/alerts-advisories/test-advisory", CancellationToken.None);
|
||||
document.Should().NotBeNull();
|
||||
document!.Status.Should().Be(DocumentStatuses.PendingParse);
|
||||
document.Metadata.Should().ContainKey("cccs.language").WhoseValue.Should().Be("en");
|
||||
[Fact]
|
||||
public async Task Fetch_PersistsRawDocumentWithMetadata()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
SeedFeedResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<CccsConnector>();
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var documentStore = harness.ServiceProvider.GetRequiredService<IDocumentStore>();
|
||||
var document = await documentStore.FindBySourceAndUriAsync(CccsConnectorPlugin.SourceName, "https://www.cyber.gc.ca/en/alerts-advisories/test-advisory", CancellationToken.None);
|
||||
document.Should().NotBeNull();
|
||||
document!.Status.Should().Be(DocumentStatuses.PendingParse);
|
||||
document.Metadata.Should().ContainKey("cccs.language").WhoseValue.Should().Be("en");
|
||||
document.Metadata.Should().ContainKey("cccs.serialNumber").WhoseValue.Should().Be("TEST-001");
|
||||
document.ContentType.Should().Be("application/json");
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddCccsConnector(options =>
|
||||
{
|
||||
options.Feeds.Clear();
|
||||
options.Feeds.Add(new CccsFeedEndpoint("en", FeedUri));
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
options.MaxEntriesPerFetch = 10;
|
||||
options.MaxKnownEntries = 32;
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(CccsOptions.HttpClientName, builderOptions =>
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
|
||||
{
|
||||
builder.PrimaryHandler = _handler;
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private void SeedFeedResponses()
|
||||
{
|
||||
AddJsonResponse(FeedUri, ReadFixture("cccs-feed-en.json"));
|
||||
AddJsonResponse(TaxonomyUri, ReadFixture("cccs-taxonomy-en.json"));
|
||||
}
|
||||
|
||||
private void AddJsonResponse(Uri uri, string json, string? etag = null)
|
||||
{
|
||||
_handler.AddResponse(uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json"),
|
||||
private async Task<ConnectorTestHarness> BuildHarnessAsync()
|
||||
{
|
||||
var initialTime = new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero);
|
||||
var harness = new ConnectorTestHarness(_fixture, initialTime, CccsOptions.HttpClientName);
|
||||
await harness.EnsureServiceProviderAsync(services =>
|
||||
{
|
||||
services.AddCccsConnector(options =>
|
||||
{
|
||||
options.Feeds.Clear();
|
||||
options.Feeds.Add(new CccsFeedEndpoint("en", FeedUri));
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
options.MaxEntriesPerFetch = 10;
|
||||
options.MaxKnownEntries = 32;
|
||||
});
|
||||
});
|
||||
return harness;
|
||||
}
|
||||
|
||||
private static void SeedFeedResponses(CannedHttpMessageHandler handler)
|
||||
{
|
||||
AddJsonResponse(handler, FeedUri, ReadFixture("cccs-feed-en.json"));
|
||||
AddJsonResponse(handler, TaxonomyUri, ReadFixture("cccs-taxonomy-en.json"));
|
||||
}
|
||||
|
||||
private static void AddJsonResponse(CannedHttpMessageHandler handler, Uri uri, string json, string? etag = null)
|
||||
{
|
||||
handler.AddResponse(uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json"),
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(etag))
|
||||
{
|
||||
response.Headers.ETag = new EntityTagHeaderValue(etag);
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private static string ReadFixture(string fileName)
|
||||
=> System.IO.File.ReadAllText(System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName));
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private static string ReadFixture(string fileName)
|
||||
=> System.IO.File.ReadAllText(System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ using StellaOps.Concelier.Connector.Cccs.Internal;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Html;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Cccs.Tests.Internal;
|
||||
|
||||
@@ -1,61 +1,51 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Connector.CertBund.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.CertBund.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.CertBund.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class CertBundConnectorTests : IAsyncLifetime
|
||||
{
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class CertBundConnectorTests
|
||||
{
|
||||
private static readonly Uri FeedUri = new("https://test.local/content/public/securityAdvisory/rss");
|
||||
private static readonly Uri PortalUri = new("https://test.local/portal/");
|
||||
private static readonly Uri DetailUri = new("https://test.local/portal/api/securityadvisory?name=WID-SEC-2025-2264");
|
||||
private static readonly Uri DetailUri = new("https://test.local/portal/api/securityadvisory?name=WID-SEC-2025-2264");
|
||||
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
|
||||
public CertBundConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
|
||||
public CertBundConnectorTests(MongoIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesCanonicalAdvisory()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<CertBundConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(5, CancellationToken.None);
|
||||
advisories.Should().HaveCount(1);
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesCanonicalAdvisory()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
SeedResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<CertBundConnector>();
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = harness.ServiceProvider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(5, CancellationToken.None);
|
||||
advisories.Should().HaveCount(1);
|
||||
|
||||
var advisory = advisories[0];
|
||||
advisory.AdvisoryKey.Should().Be("WID-SEC-2025-2264");
|
||||
@@ -75,9 +65,9 @@ public sealed class CertBundConnectorTests : IAsyncLifetime
|
||||
rule.Max == "2024.2" &&
|
||||
rule.Notes == "certbund:WID-SEC-2025-2264:ivanti");
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(CertBundConnectorPlugin.SourceName, CancellationToken.None);
|
||||
state.Should().NotBeNull();
|
||||
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(CertBundConnectorPlugin.SourceName, CancellationToken.None);
|
||||
state.Should().NotBeNull();
|
||||
state!.Cursor.Should().NotBeNull();
|
||||
state.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue();
|
||||
pendingDocs!.AsBsonArray.Should().BeEmpty();
|
||||
@@ -85,86 +75,64 @@ public sealed class CertBundConnectorTests : IAsyncLifetime
|
||||
pendingMappings!.AsBsonArray.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Fetch_PersistsDocumentWithMetadata()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<CertBundConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
|
||||
var documentStore = provider.GetRequiredService<IDocumentStore>();
|
||||
var document = await documentStore.FindBySourceAndUriAsync(CertBundConnectorPlugin.SourceName, DetailUri.ToString(), CancellationToken.None);
|
||||
document.Should().NotBeNull();
|
||||
[Fact]
|
||||
public async Task Fetch_PersistsDocumentWithMetadata()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
SeedResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<CertBundConnector>();
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var documentStore = harness.ServiceProvider.GetRequiredService<IDocumentStore>();
|
||||
var document = await documentStore.FindBySourceAndUriAsync(CertBundConnectorPlugin.SourceName, DetailUri.ToString(), CancellationToken.None);
|
||||
document.Should().NotBeNull();
|
||||
document!.Metadata.Should().ContainKey("certbund.advisoryId").WhoseValue.Should().Be("WID-SEC-2025-2264");
|
||||
document.Metadata.Should().ContainKey("certbund.category");
|
||||
document.Metadata.Should().ContainKey("certbund.published");
|
||||
document.Status.Should().Be(DocumentStatuses.PendingParse);
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(CertBundConnectorPlugin.SourceName, CancellationToken.None);
|
||||
state.Should().NotBeNull();
|
||||
document.Metadata.Should().ContainKey("certbund.published");
|
||||
document.Status.Should().Be(DocumentStatuses.PendingParse);
|
||||
|
||||
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(CertBundConnectorPlugin.SourceName, CancellationToken.None);
|
||||
state.Should().NotBeNull();
|
||||
state!.Cursor.Should().NotBeNull();
|
||||
state.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue();
|
||||
pendingDocs!.AsBsonArray.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddCertBundConnector(options =>
|
||||
{
|
||||
options.FeedUri = FeedUri;
|
||||
options.PortalBootstrapUri = PortalUri;
|
||||
options.DetailApiUri = new Uri("https://test.local/portal/api/securityadvisory");
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
options.MaxAdvisoriesPerFetch = 10;
|
||||
options.MaxKnownAdvisories = 32;
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(CertBundOptions.HttpClientName, builderOptions =>
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
|
||||
{
|
||||
builder.PrimaryHandler = _handler;
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private void SeedResponses()
|
||||
{
|
||||
AddJsonResponse(DetailUri, ReadFixture("certbund-detail.json"));
|
||||
AddXmlResponse(FeedUri, ReadFixture("certbund-feed.xml"), "application/rss+xml");
|
||||
AddHtmlResponse(PortalUri, "<html><body>OK</body></html>");
|
||||
}
|
||||
|
||||
private void AddJsonResponse(Uri uri, string json, string? etag = null)
|
||||
{
|
||||
_handler.AddResponse(uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json"),
|
||||
state.Cursor.TryGetValue("pendingDocuments", out var pendingDocs).Should().BeTrue();
|
||||
pendingDocs!.AsBsonArray.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
private async Task<ConnectorTestHarness> BuildHarnessAsync()
|
||||
{
|
||||
var initialTime = new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero);
|
||||
var harness = new ConnectorTestHarness(_fixture, initialTime, CertBundOptions.HttpClientName);
|
||||
await harness.EnsureServiceProviderAsync(services =>
|
||||
{
|
||||
services.AddCertBundConnector(options =>
|
||||
{
|
||||
options.FeedUri = FeedUri;
|
||||
options.PortalBootstrapUri = PortalUri;
|
||||
options.DetailApiUri = new Uri("https://test.local/portal/api/securityadvisory");
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
options.MaxAdvisoriesPerFetch = 10;
|
||||
options.MaxKnownAdvisories = 32;
|
||||
});
|
||||
});
|
||||
return harness;
|
||||
}
|
||||
|
||||
private static void SeedResponses(CannedHttpMessageHandler handler)
|
||||
{
|
||||
AddJsonResponse(handler, DetailUri, ReadFixture("certbund-detail.json"));
|
||||
AddXmlResponse(handler, FeedUri, ReadFixture("certbund-feed.xml"), "application/rss+xml");
|
||||
AddHtmlResponse(handler, PortalUri, "<html><body>OK</body></html>");
|
||||
}
|
||||
|
||||
private static void AddJsonResponse(CannedHttpMessageHandler handler, Uri uri, string json, string? etag = null)
|
||||
{
|
||||
handler.AddResponse(uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json"),
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(etag))
|
||||
{
|
||||
@@ -172,29 +140,25 @@ public sealed class CertBundConnectorTests : IAsyncLifetime
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddXmlResponse(CannedHttpMessageHandler handler, Uri uri, string xml, string contentType)
|
||||
{
|
||||
handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(xml, Encoding.UTF8, contentType),
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddHtmlResponse(CannedHttpMessageHandler handler, Uri uri, string html)
|
||||
{
|
||||
handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(html, Encoding.UTF8, "text/html"),
|
||||
});
|
||||
}
|
||||
|
||||
private void AddXmlResponse(Uri uri, string xml, string contentType)
|
||||
{
|
||||
_handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(xml, Encoding.UTF8, contentType),
|
||||
});
|
||||
}
|
||||
|
||||
private void AddHtmlResponse(Uri uri, string html)
|
||||
{
|
||||
_handler.AddResponse(uri, () => new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(html, Encoding.UTF8, "text/html"),
|
||||
});
|
||||
}
|
||||
|
||||
private static string ReadFixture(string fileName)
|
||||
=> System.IO.File.ReadAllText(System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName));
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
}
|
||||
private static string ReadFixture(string fileName)
|
||||
=> System.IO.File.ReadAllText(System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName));
|
||||
}
|
||||
|
||||
@@ -6,35 +6,36 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Connector.CertCc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.CertCc;
|
||||
using StellaOps.Concelier.Connector.CertCc.Configuration;
|
||||
using StellaOps.Concelier.Connector.CertCc.Internal;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Cursors;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using StellaOps.Concelier.Connector.Common.Cursors;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.CertCc.Tests.CertCc;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class CertCcConnectorFetchTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestNoteId = "294418";
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
private ServiceProvider? _serviceProvider;
|
||||
|
||||
public CertCcConnectorFetchTests(MongoIntegrationFixture fixture)
|
||||
public CertCcConnectorFetchTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 11, 8, 0, 0, TimeSpan.Zero));
|
||||
@@ -172,25 +173,22 @@ public sealed class CertCcConnectorFetchTests : IAsyncLifetime
|
||||
yield return new Uri(baseUri, $"{TestNoteId}/vuls/");
|
||||
}
|
||||
|
||||
private async Task EnsureServiceProviderAsync(CertCcOptions template)
|
||||
{
|
||||
await DisposeServiceProviderAsync();
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
private async Task EnsureServiceProviderAsync(CertCcOptions template)
|
||||
{
|
||||
await DisposeServiceProviderAsync();
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
options.RawDocumentRetention = TimeSpan.Zero;
|
||||
options.RawDocumentRetentionTtlGrace = TimeSpan.FromMinutes(5);
|
||||
options.RawDocumentRetentionSweepInterval = TimeSpan.FromHours(1);
|
||||
});
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddCertCcConnector(options =>
|
||||
@@ -212,12 +210,10 @@ public sealed class CertCcConnectorFetchTests : IAsyncLifetime
|
||||
services.Configure<HttpClientFactoryOptions>(CertCcOptions.HttpClientName, builderOptions =>
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler);
|
||||
});
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
var bootstrapper = _serviceProvider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
}
|
||||
});
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private async Task DisposeServiceProviderAsync()
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.CertCc;
|
||||
using StellaOps.Concelier.Connector.CertCc.Configuration;
|
||||
@@ -22,15 +22,15 @@ using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Cursors;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.CertCc.Tests.CertCc;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class CertCcConnectorSnapshotTests : IAsyncLifetime
|
||||
{
|
||||
private static readonly Uri SeptemberSummaryUri = new("https://www.kb.cert.org/vuls/api/2025/09/summary/");
|
||||
@@ -43,10 +43,10 @@ public sealed class CertCcConnectorSnapshotTests : IAsyncLifetime
|
||||
|
||||
private static readonly Uri YearlySummaryUri = new("https://www.kb.cert.org/vuls/api/2025/summary/");
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private ConnectorTestHarness? _harness;
|
||||
|
||||
public CertCcConnectorSnapshotTests(MongoIntegrationFixture fixture)
|
||||
public CertCcConnectorSnapshotTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
@@ -10,26 +10,26 @@ using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Connector.CertCc;
|
||||
using StellaOps.Concelier.Connector.CertCc.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Cursors;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Connector.CertCc;
|
||||
using StellaOps.Concelier.Connector.CertCc.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Cursors;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.CertCc.Tests.CertCc;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class CertCcConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private static readonly Uri MonthlySummaryUri = new("https://www.kb.cert.org/vuls/api/2025/10/summary/");
|
||||
@@ -39,11 +39,11 @@ public sealed class CertCcConnectorTests : IAsyncLifetime
|
||||
private static readonly Uri VulsUri = new("https://www.kb.cert.org/vuls/api/294418/vuls/");
|
||||
private static readonly Uri VendorStatusesUri = new("https://www.kb.cert.org/vuls/api/294418/vendors/vuls/");
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
|
||||
public CertCcConnectorTests(MongoIntegrationFixture fixture)
|
||||
public CertCcConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 11, 9, 30, 0, TimeSpan.Zero));
|
||||
@@ -269,10 +269,10 @@ public sealed class CertCcConnectorTests : IAsyncLifetime
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
}
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAndMap_SkipWhenDetailMappingDisabled()
|
||||
@@ -308,22 +308,22 @@ public sealed class CertCcConnectorTests : IAsyncLifetime
|
||||
pendingMappings.Should().Be(0);
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync(bool enableDetailMapping = true)
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync(bool enableDetailMapping = true)
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddCertCcConnector(options =>
|
||||
@@ -350,11 +350,8 @@ public sealed class CertCcConnectorTests : IAsyncLifetime
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private void SeedSummaryResponses(string summaryEtag = "\"summary-oct\"", string yearlyEtag = "\"summary-year\"")
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.CertCc.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.CertCc.Tests.Internal;
|
||||
|
||||
@@ -3,66 +3,53 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Connector.CertFr;
|
||||
using StellaOps.Concelier.Connector.CertFr.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.CertFr;
|
||||
using StellaOps.Concelier.Connector.CertFr.Configuration;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.CertFr.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class CertFrConnectorTests : IAsyncLifetime
|
||||
{
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class CertFrConnectorTests
|
||||
{
|
||||
private static readonly Uri FeedUri = new("https://www.cert.ssi.gouv.fr/feed/alertes/");
|
||||
private static readonly Uri FirstDetailUri = new("https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/");
|
||||
private static readonly Uri SecondDetailUri = new("https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/");
|
||||
private static readonly Uri SecondDetailUri = new("https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/");
|
||||
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
|
||||
public CertFrConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
|
||||
public CertFrConnectorTests(MongoIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 10, 3, 0, 0, 0, TimeSpan.Zero));
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesDeterministicSnapshot()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedFeed();
|
||||
SeedDetailResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<CertFrConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesDeterministicSnapshot()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
SeedFeed(harness.Handler);
|
||||
SeedDetailResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<CertFrConnector>();
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
harness.TimeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = harness.ServiceProvider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
|
||||
var snapshot = SnapshotSerializer.ToSnapshot(advisories.OrderBy(static a => a.AdvisoryKey, StringComparer.Ordinal).ToArray());
|
||||
var expected = ReadFixture("certfr-advisories.snapshot.json");
|
||||
@@ -77,216 +64,193 @@ public sealed class CertFrConnectorTests : IAsyncLifetime
|
||||
|
||||
Assert.Equal(normalizedExpected, normalizedSnapshot);
|
||||
|
||||
var documentStore = provider.GetRequiredService<IDocumentStore>();
|
||||
var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(firstDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);
|
||||
var documentStore = harness.ServiceProvider.GetRequiredService<IDocumentStore>();
|
||||
var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(firstDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);
|
||||
|
||||
var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(secondDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);
|
||||
var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(secondDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);
|
||||
|
||||
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.AsBsonArray.Count == 0);
|
||||
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0);
|
||||
}
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0);
|
||||
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0);
|
||||
}
|
||||
[Fact]
|
||||
public async Task FetchFailure_RecordsBackoffAndReason()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
harness.Handler.AddResponse(FeedUri, () => new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||
{
|
||||
Content = new StringContent("feed error", Encoding.UTF8, "text/plain"),
|
||||
});
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<CertFrConnector>();
|
||||
await Assert.ThrowsAsync<HttpRequestException>(() => connector.FetchAsync(harness.ServiceProvider, CancellationToken.None));
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchFailure_RecordsBackoffAndReason()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
_handler.AddResponse(FeedUri, () => new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||
{
|
||||
Content = new StringContent("feed error", Encoding.UTF8, "text/plain"),
|
||||
});
|
||||
[Fact]
|
||||
public async Task Fetch_NotModifiedResponsesMaintainDocumentState()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
SeedFeed(harness.Handler);
|
||||
SeedDetailResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<CertFrConnector>();
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var documentStore = harness.ServiceProvider.GetRequiredService<IDocumentStore>();
|
||||
var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(firstDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);
|
||||
|
||||
var connector = provider.GetRequiredService<CertFrConnector>();
|
||||
await Assert.ThrowsAsync<HttpRequestException>(() => connector.FetchAsync(provider, CancellationToken.None));
|
||||
var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(secondDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);
|
||||
|
||||
SeedFeed(harness.Handler);
|
||||
SeedNotModifiedDetailResponses(harness.Handler);
|
||||
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(firstDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);
|
||||
|
||||
var stateRepository = provider.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 > _timeProvider.GetUtcNow());
|
||||
}
|
||||
secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(secondDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);
|
||||
|
||||
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.AsBsonArray.Count == 0);
|
||||
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Fetch_NotModifiedResponsesMaintainDocumentState()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedFeed();
|
||||
SeedDetailResponses();
|
||||
[Fact]
|
||||
public async Task Fetch_DuplicateContentSkipsRequeue()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
SeedFeed(harness.Handler);
|
||||
SeedDetailResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<CertFrConnector>();
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var documentStore = harness.ServiceProvider.GetRequiredService<IDocumentStore>();
|
||||
var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(firstDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);
|
||||
|
||||
var connector = provider.GetRequiredService<CertFrConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(secondDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);
|
||||
|
||||
SeedFeed(harness.Handler);
|
||||
SeedDetailResponses(harness.Handler);
|
||||
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(firstDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);
|
||||
|
||||
var documentStore = provider.GetRequiredService<IDocumentStore>();
|
||||
var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(firstDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);
|
||||
secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(secondDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);
|
||||
|
||||
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.AsBsonArray.Count == 0);
|
||||
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0);
|
||||
}
|
||||
|
||||
var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(secondDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);
|
||||
|
||||
SeedFeed();
|
||||
SeedNotModifiedDetailResponses();
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
|
||||
firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(firstDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);
|
||||
|
||||
secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(secondDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0);
|
||||
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Fetch_DuplicateContentSkipsRequeue()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
SeedFeed();
|
||||
SeedDetailResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<CertFrConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var documentStore = provider.GetRequiredService<IDocumentStore>();
|
||||
var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(firstDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);
|
||||
|
||||
var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(secondDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);
|
||||
|
||||
SeedFeed();
|
||||
SeedDetailResponses();
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(firstDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);
|
||||
|
||||
secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(secondDocument);
|
||||
Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0);
|
||||
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0);
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddCertFrConnector(opts =>
|
||||
{
|
||||
opts.FeedUri = FeedUri;
|
||||
opts.InitialBackfill = TimeSpan.FromDays(30);
|
||||
opts.WindowOverlap = TimeSpan.FromDays(2);
|
||||
opts.MaxItemsPerFetch = 50;
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(CertFrOptions.HttpClientName, builderOptions =>
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
|
||||
{
|
||||
builder.PrimaryHandler = _handler;
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private void SeedFeed()
|
||||
{
|
||||
_handler.AddTextResponse(FeedUri, ReadFixture("certfr-feed.xml"), "application/atom+xml");
|
||||
}
|
||||
|
||||
private void SeedDetailResponses()
|
||||
{
|
||||
AddDetailResponse(FirstDetailUri, "certfr-detail-AV-2024-001.html", "\"certfr-001\"");
|
||||
AddDetailResponse(SecondDetailUri, "certfr-detail-AV-2024-002.html", "\"certfr-002\"");
|
||||
}
|
||||
|
||||
private void SeedNotModifiedDetailResponses()
|
||||
{
|
||||
AddNotModifiedResponse(FirstDetailUri, "\"certfr-001\"");
|
||||
AddNotModifiedResponse(SecondDetailUri, "\"certfr-002\"");
|
||||
}
|
||||
|
||||
private void AddDetailResponse(Uri uri, string fixture, string? etag)
|
||||
{
|
||||
_handler.AddResponse(uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(ReadFixture(fixture), Encoding.UTF8, "text/html"),
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(etag))
|
||||
{
|
||||
response.Headers.ETag = new EntityTagHeaderValue(etag);
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private void AddNotModifiedResponse(Uri uri, string? etag)
|
||||
{
|
||||
_handler.AddResponse(uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.NotModified);
|
||||
if (!string.IsNullOrEmpty(etag))
|
||||
{
|
||||
response.Headers.ETag = new EntityTagHeaderValue(etag);
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
private async Task<ConnectorTestHarness> BuildHarnessAsync()
|
||||
{
|
||||
var initialTime = new DateTimeOffset(2024, 10, 3, 0, 0, 0, TimeSpan.Zero);
|
||||
var harness = new ConnectorTestHarness(_fixture, initialTime, CertFrOptions.HttpClientName);
|
||||
await harness.EnsureServiceProviderAsync(services =>
|
||||
{
|
||||
services.AddCertFrConnector(opts =>
|
||||
{
|
||||
opts.FeedUri = FeedUri;
|
||||
opts.InitialBackfill = TimeSpan.FromDays(30);
|
||||
opts.WindowOverlap = TimeSpan.FromDays(2);
|
||||
opts.MaxItemsPerFetch = 50;
|
||||
});
|
||||
});
|
||||
return harness;
|
||||
}
|
||||
|
||||
private void SeedFeed(CannedHttpMessageHandler handler)
|
||||
{
|
||||
handler.AddTextResponse(FeedUri, ReadFixture("certfr-feed.xml"), "application/atom+xml");
|
||||
}
|
||||
|
||||
private void SeedDetailResponses(CannedHttpMessageHandler handler)
|
||||
{
|
||||
AddDetailResponse(handler, FirstDetailUri, "certfr-detail-AV-2024-001.html", "\"certfr-001\"");
|
||||
AddDetailResponse(handler, SecondDetailUri, "certfr-detail-AV-2024-002.html", "\"certfr-002\"");
|
||||
}
|
||||
|
||||
private void SeedNotModifiedDetailResponses(CannedHttpMessageHandler handler)
|
||||
{
|
||||
AddNotModifiedResponse(handler, FirstDetailUri, "\"certfr-001\"");
|
||||
AddNotModifiedResponse(handler, SecondDetailUri, "\"certfr-002\"");
|
||||
}
|
||||
|
||||
private static void AddDetailResponse(CannedHttpMessageHandler handler, Uri uri, string fixture, string? etag)
|
||||
{
|
||||
handler.AddResponse(uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(ReadFixture(fixture), Encoding.UTF8, "text/html"),
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(etag))
|
||||
{
|
||||
response.Headers.ETag = new EntityTagHeaderValue(etag);
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddNotModifiedResponse(CannedHttpMessageHandler handler, Uri uri, string? etag)
|
||||
{
|
||||
handler.AddResponse(uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.NotModified);
|
||||
if (!string.IsNullOrEmpty(etag))
|
||||
{
|
||||
response.Headers.ETag = new EntityTagHeaderValue(etag);
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private static string ReadFixture(string filename)
|
||||
{
|
||||
@@ -304,10 +268,4 @@ public sealed class CertFrConnectorTests : IAsyncLifetime
|
||||
private static string Normalize(string value)
|
||||
=> value.Replace("\r\n", "\n", StringComparison.Ordinal);
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,32 +13,33 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.CertIn;
|
||||
using StellaOps.Concelier.Connector.CertIn.Configuration;
|
||||
using StellaOps.Concelier.Connector.CertIn.Internal;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.CertIn.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class CertInConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
private ServiceProvider? _serviceProvider;
|
||||
|
||||
public CertInConnectorTests(MongoIntegrationFixture fixture)
|
||||
public CertInConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 4, 20, 0, 0, 0, TimeSpan.Zero));
|
||||
@@ -275,24 +276,24 @@ public sealed class CertInConnectorTests : IAsyncLifetime
|
||||
await ResetDatabaseAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddCertInConnector(opts =>
|
||||
{
|
||||
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddCertInConnector(opts =>
|
||||
{
|
||||
opts.AlertsEndpoint = template.AlertsEndpoint;
|
||||
opts.WindowSize = template.WindowSize;
|
||||
opts.WindowOverlap = template.WindowOverlap;
|
||||
@@ -306,15 +307,13 @@ public sealed class CertInConnectorTests : IAsyncLifetime
|
||||
{
|
||||
builder.PrimaryHandler = _handler;
|
||||
});
|
||||
});
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
var bootstrapper = _serviceProvider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
private Task ResetDatabaseAsync()
|
||||
=> _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
});
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private Task ResetDatabaseAsync()
|
||||
=> _fixture.TruncateAllTablesAsync();
|
||||
|
||||
private static string ReadFixture(string filename)
|
||||
=> File.ReadAllText(ResolveFixturePath(filename));
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Text;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Mongo2Go;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Aoc;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
@@ -13,8 +13,8 @@ using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Core.Aoc;
|
||||
using StellaOps.Concelier.Core.Linksets;
|
||||
using StellaOps.Concelier.RawModels;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Common.Tests;
|
||||
|
||||
@@ -2,15 +2,15 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Mongo2Go;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using MongoDB.Driver;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.State;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Common.Tests;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.Common.Cursors;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Common.Tests;
|
||||
|
||||
@@ -7,7 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
@@ -15,22 +15,22 @@ using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Cve.Configuration;
|
||||
using StellaOps.Concelier.Connector.Cve.Internal;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Cve.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class CveConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private ConnectorTestHarness? _harness;
|
||||
|
||||
public CveConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
|
||||
public CveConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_output = output;
|
||||
|
||||
@@ -10,40 +10,40 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Distro.Debian.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Debian.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class DebianConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private static readonly Uri ListUri = new("https://salsa.debian.org/security-tracker-team/security-tracker/-/raw/master/data/DSA/list");
|
||||
private static readonly Uri DetailResolved = new("https://security-tracker.debian.org/tracker/DSA-2024-123");
|
||||
private static readonly Uri DetailOpen = new("https://security-tracker.debian.org/tracker/DSA-2024-124");
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
private readonly Dictionary<Uri, Func<HttpRequestMessage, HttpResponseMessage>> _fallbackFactories = new();
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public DebianConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
|
||||
public DebianConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
@@ -103,7 +103,7 @@ public sealed class DebianConnectorTests : IAsyncLifetime
|
||||
Assert.NotNull(openRange.Primitives);
|
||||
Assert.NotNull(openRange.Primitives!.Evr);
|
||||
|
||||
// Ensure data persisted through Mongo round-trip.
|
||||
// Ensure data persisted through storage round-trip.
|
||||
var found = await advisoryStore.FindAsync("DSA-2024-123", CancellationToken.None);
|
||||
Assert.NotNull(found);
|
||||
var persistedRange = Assert.Single(found!.AffectedPackages, pkg => pkg.Platform == "bookworm").VersionRanges.Single();
|
||||
@@ -125,23 +125,23 @@ public sealed class DebianConnectorTests : IAsyncLifetime
|
||||
Assert.Equal(2, refreshed.Count);
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName, CancellationToken.None);
|
||||
_handler.Clear();
|
||||
_fallbackFactories.Clear();
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync(CancellationToken.None);
|
||||
_handler.Clear();
|
||||
_fallbackFactories.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(new TestOutputLoggerProvider(_output)));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddDebianConnector(options =>
|
||||
@@ -160,11 +160,8 @@ public sealed class DebianConnectorTests : IAsyncLifetime
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private void SeedInitialResponses()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using Xunit;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Distro.Debian;
|
||||
using StellaOps.Concelier.Connector.Distro.Debian.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Debian.Tests;
|
||||
|
||||
|
||||
@@ -2,23 +2,23 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Distro.RedHat;
|
||||
using StellaOps.Concelier.Connector.Distro.RedHat.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.RedHat.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class RedHatConnectorHarnessTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ConnectorTestHarness _harness;
|
||||
|
||||
public RedHatConnectorHarnessTests(MongoIntegrationFixture fixture)
|
||||
public RedHatConnectorHarnessTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_harness = new ConnectorTestHarness(fixture, new DateTimeOffset(2025, 10, 5, 0, 0, 0, TimeSpan.Zero), RedHatOptions.HttpClientName);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
@@ -23,10 +23,11 @@ using StellaOps.Concelier.Connector.Distro.RedHat;
|
||||
using StellaOps.Concelier.Connector.Distro.RedHat.Configuration;
|
||||
using StellaOps.Concelier.Connector.Distro.RedHat.Internal;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Plugin;
|
||||
using Xunit;
|
||||
@@ -34,10 +35,10 @@ using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.RedHat.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly DateTimeOffset _initialNow;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
@@ -45,7 +46,7 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
private ServiceProvider? _serviceProvider;
|
||||
private const bool ForceUpdateGoldens = false;
|
||||
|
||||
public RedHatConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
|
||||
public RedHatConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_initialNow = new DateTimeOffset(2025, 10, 5, 0, 0, 0, TimeSpan.Zero);
|
||||
@@ -544,11 +545,11 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(handler);
|
||||
|
||||
services.AddMongoStorage(storageOptions =>
|
||||
services.AddConcelierPostgresStorage(storageOptions =>
|
||||
{
|
||||
storageOptions.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
storageOptions.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
storageOptions.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
storageOptions.ConnectionString = _fixture.ConnectionString;
|
||||
storageOptions.SchemaName = _fixture.SchemaName;
|
||||
storageOptions.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
@@ -584,10 +585,7 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private Task ResetDatabaseAsync()
|
||||
@@ -611,7 +609,7 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
_serviceProvider = null;
|
||||
}
|
||||
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
_handler.Clear();
|
||||
_timeProvider.SetUtcNow(_initialNow);
|
||||
}
|
||||
|
||||
@@ -4,64 +4,53 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse.Configuration;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Suse.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class SuseConnectorTests : IAsyncLifetime
|
||||
{
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class SuseConnectorTests
|
||||
{
|
||||
private static readonly Uri ChangesUri = new("https://ftp.suse.com/pub/projects/security/csaf/changes.csv");
|
||||
private static readonly Uri AdvisoryResolvedUri = new("https://ftp.suse.com/pub/projects/security/csaf/suse-su-2025_0001-1.json");
|
||||
private static readonly Uri AdvisoryOpenUri = new("https://ftp.suse.com/pub/projects/security/csaf/suse-su-2025_0002-1.json");
|
||||
private static readonly Uri AdvisoryOpenUri = new("https://ftp.suse.com/pub/projects/security/csaf/suse-su-2025_0002-1.json");
|
||||
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
|
||||
public SuseConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
|
||||
public SuseConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 22, 0, 0, 0, TimeSpan.Zero));
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProcessesResolvedAndOpenNotices()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
|
||||
SeedInitialResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<SuseConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProcessesResolvedAndOpenNotices()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
|
||||
SeedInitialResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<SuseConnector>();
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
harness.TimeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = harness.ServiceProvider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
|
||||
var resolved = advisories.Single(a => a.AdvisoryKey == "SUSE-SU-2025:0001-1");
|
||||
var resolvedPackage = Assert.Single(resolved.AffectedPackages);
|
||||
@@ -71,77 +60,54 @@ public sealed class SuseConnectorTests : IAsyncLifetime
|
||||
Assert.NotNull(resolvedRange.Primitives!.Nevra?.Fixed);
|
||||
|
||||
var open = advisories.Single(a => a.AdvisoryKey == "SUSE-SU-2025:0002-1");
|
||||
var openPackage = Assert.Single(open.AffectedPackages);
|
||||
Assert.Equal(AffectedPackageStatusCatalog.UnderInvestigation, openPackage.Statuses.Single().Status);
|
||||
|
||||
SeedNotModifiedResponses();
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
_handler.AssertNoPendingResponses();
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddSuseConnector(options =>
|
||||
{
|
||||
options.ChangesEndpoint = ChangesUri;
|
||||
options.AdvisoryBaseUri = new Uri("https://ftp.suse.com/pub/projects/security/csaf/");
|
||||
options.MaxAdvisoriesPerFetch = 5;
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(SuseOptions.HttpClientName, builderOptions =>
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
|
||||
{
|
||||
builder.PrimaryHandler = _handler;
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private void SeedInitialResponses()
|
||||
{
|
||||
_handler.AddResponse(ChangesUri, () => BuildResponse(HttpStatusCode.OK, "suse-changes.csv", "\"changes-v1\""));
|
||||
_handler.AddResponse(AdvisoryResolvedUri, () => BuildResponse(HttpStatusCode.OK, "suse-su-2025_0001-1.json", "\"adv-1\""));
|
||||
_handler.AddResponse(AdvisoryOpenUri, () => BuildResponse(HttpStatusCode.OK, "suse-su-2025_0002-1.json", "\"adv-2\""));
|
||||
}
|
||||
|
||||
private void SeedNotModifiedResponses()
|
||||
{
|
||||
_handler.AddResponse(ChangesUri, () => BuildResponse(HttpStatusCode.NotModified, "suse-changes.csv", "\"changes-v1\""));
|
||||
}
|
||||
|
||||
private HttpResponseMessage BuildResponse(HttpStatusCode statusCode, string fixture, string etag)
|
||||
{
|
||||
var response = new HttpResponseMessage(statusCode);
|
||||
if (statusCode == HttpStatusCode.OK)
|
||||
var openPackage = Assert.Single(open.AffectedPackages);
|
||||
Assert.Equal(AffectedPackageStatusCatalog.UnderInvestigation, openPackage.Statuses.Single().Status);
|
||||
|
||||
SeedNotModifiedResponses(harness.Handler);
|
||||
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
harness.TimeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
harness.Handler.AssertNoPendingResponses();
|
||||
}
|
||||
|
||||
private async Task<ConnectorTestHarness> BuildHarnessAsync()
|
||||
{
|
||||
var initialTime = new DateTimeOffset(2025, 1, 22, 0, 0, 0, TimeSpan.Zero);
|
||||
var harness = new ConnectorTestHarness(_fixture, initialTime, SuseOptions.HttpClientName);
|
||||
await harness.EnsureServiceProviderAsync(services =>
|
||||
{
|
||||
services.AddSuseConnector(options =>
|
||||
{
|
||||
options.ChangesEndpoint = ChangesUri;
|
||||
options.AdvisoryBaseUri = new Uri("https://ftp.suse.com/pub/projects/security/csaf/");
|
||||
options.MaxAdvisoriesPerFetch = 5;
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
});
|
||||
});
|
||||
return harness;
|
||||
}
|
||||
|
||||
private static void SeedInitialResponses(CannedHttpMessageHandler handler)
|
||||
{
|
||||
handler.AddResponse(ChangesUri, () => BuildResponse(HttpStatusCode.OK, "suse-changes.csv", "\"changes-v1\""));
|
||||
handler.AddResponse(AdvisoryResolvedUri, () => BuildResponse(HttpStatusCode.OK, "suse-su-2025_0001-1.json", "\"adv-1\""));
|
||||
handler.AddResponse(AdvisoryOpenUri, () => BuildResponse(HttpStatusCode.OK, "suse-su-2025_0002-1.json", "\"adv-2\""));
|
||||
}
|
||||
|
||||
private static void SeedNotModifiedResponses(CannedHttpMessageHandler handler)
|
||||
{
|
||||
handler.AddResponse(ChangesUri, () => BuildResponse(HttpStatusCode.NotModified, "suse-changes.csv", "\"changes-v1\""));
|
||||
}
|
||||
|
||||
private static HttpResponseMessage BuildResponse(HttpStatusCode statusCode, string fixture, string etag)
|
||||
{
|
||||
var response = new HttpResponseMessage(statusCode);
|
||||
if (statusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var contentType = fixture.EndsWith(".csv", StringComparison.OrdinalIgnoreCase) ? "text/csv" : "application/json";
|
||||
response.Content = new StringContent(ReadFixture(Path.Combine("Source", "Distro", "Suse", "Fixtures", fixture)), Encoding.UTF8, contentType);
|
||||
@@ -158,11 +124,7 @@ public sealed class SuseConnectorTests : IAsyncLifetime
|
||||
{
|
||||
throw new FileNotFoundException($"Fixture '{relativePath}' not found.", path);
|
||||
}
|
||||
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
}
|
||||
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Suse.Tests;
|
||||
|
||||
@@ -4,61 +4,52 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Distro.Ubuntu;
|
||||
using StellaOps.Concelier.Connector.Distro.Ubuntu.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Distro.Ubuntu;
|
||||
using StellaOps.Concelier.Connector.Distro.Ubuntu.Configuration;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using Xunit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Ubuntu.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class UbuntuConnectorTests : IAsyncLifetime
|
||||
{
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class UbuntuConnectorTests
|
||||
{
|
||||
private static readonly Uri IndexPage0Uri = new("https://ubuntu.com/security/notices.json?offset=0&limit=1");
|
||||
private static readonly Uri IndexPage1Uri = new("https://ubuntu.com/security/notices.json?offset=1&limit=1");
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
|
||||
public UbuntuConnectorTests(MongoIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 25, 0, 0, 0, TimeSpan.Zero));
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_GeneratesEvrRangePrimitives()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
|
||||
SeedInitialResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<UbuntuConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
private static readonly Uri IndexPage1Uri = new("https://ubuntu.com/security/notices.json?offset=1&limit=1");
|
||||
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
|
||||
public UbuntuConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_GeneratesEvrRangePrimitives()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
|
||||
SeedInitialResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<UbuntuConnector>();
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
harness.TimeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = harness.ServiceProvider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
|
||||
var kernelNotice = advisories.Single(a => a.AdvisoryKey == "USN-9001-1");
|
||||
var noblePackage = Assert.Single(kernelNotice.AffectedPackages, pkg => pkg.Platform == "noble");
|
||||
@@ -73,95 +64,72 @@ public sealed class UbuntuConnectorTests : IAsyncLifetime
|
||||
Assert.Equal(range.Primitives.Evr!.Fixed!.ToCanonicalString(), normalizedRule.Max);
|
||||
Assert.Equal("ubuntu:noble", normalizedRule.Notes);
|
||||
|
||||
SeedNotModifiedResponses();
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
_handler.AssertNoPendingResponses();
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
SeedNotModifiedResponses(harness.Handler);
|
||||
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
harness.TimeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
harness.Handler.AssertNoPendingResponses();
|
||||
}
|
||||
|
||||
private async Task<ConnectorTestHarness> BuildHarnessAsync()
|
||||
{
|
||||
var initialTime = new DateTimeOffset(2025, 1, 25, 0, 0, 0, TimeSpan.Zero);
|
||||
var harness = new ConnectorTestHarness(_fixture, initialTime, UbuntuOptions.HttpClientName);
|
||||
await harness.EnsureServiceProviderAsync(services =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
services.AddStellaOpsCrypto();
|
||||
services.AddUbuntuConnector(options =>
|
||||
{
|
||||
options.NoticesEndpoint = new Uri("https://ubuntu.com/security/notices.json");
|
||||
options.NoticeDetailBaseUri = new Uri("https://ubuntu.com/security/");
|
||||
options.MaxNoticesPerFetch = 2;
|
||||
options.IndexPageSize = 1;
|
||||
});
|
||||
});
|
||||
return harness;
|
||||
}
|
||||
|
||||
private static void SeedInitialResponses(CannedHttpMessageHandler handler)
|
||||
{
|
||||
handler.AddResponse(IndexPage0Uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(ReadFixture("Fixtures/ubuntu-notices-page0.json"), Encoding.UTF8, "application/json")
|
||||
};
|
||||
response.Headers.ETag = new EntityTagHeaderValue("\"index-page0-v1\"");
|
||||
return response;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddStellaOpsCrypto();
|
||||
services.AddUbuntuConnector(options =>
|
||||
handler.AddResponse(IndexPage1Uri, () =>
|
||||
{
|
||||
options.NoticesEndpoint = new Uri("https://ubuntu.com/security/notices.json");
|
||||
options.NoticeDetailBaseUri = new Uri("https://ubuntu.com/security/");
|
||||
options.MaxNoticesPerFetch = 2;
|
||||
options.IndexPageSize = 1;
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(UbuntuOptions.HttpClientName, builderOptions =>
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
|
||||
{
|
||||
builder.PrimaryHandler = _handler;
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private void SeedInitialResponses()
|
||||
{
|
||||
_handler.AddResponse(IndexPage0Uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(ReadFixture("Fixtures/ubuntu-notices-page0.json"), Encoding.UTF8, "application/json")
|
||||
};
|
||||
response.Headers.ETag = new EntityTagHeaderValue("\"index-page0-v1\"");
|
||||
return response;
|
||||
});
|
||||
|
||||
_handler.AddResponse(IndexPage1Uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(ReadFixture("Fixtures/ubuntu-notices-page1.json"), Encoding.UTF8, "application/json")
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(ReadFixture("Fixtures/ubuntu-notices-page1.json"), Encoding.UTF8, "application/json")
|
||||
};
|
||||
response.Headers.ETag = new EntityTagHeaderValue("\"index-page1-v1\"");
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private void SeedNotModifiedResponses()
|
||||
{
|
||||
_handler.AddResponse(IndexPage0Uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.NotModified);
|
||||
response.Headers.ETag = new EntityTagHeaderValue("\"index-page0-v1\"");
|
||||
return response;
|
||||
});
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private static void SeedNotModifiedResponses(CannedHttpMessageHandler handler)
|
||||
{
|
||||
handler.AddResponse(IndexPage0Uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.NotModified);
|
||||
response.Headers.ETag = new EntityTagHeaderValue("\"index-page0-v1\"");
|
||||
return response;
|
||||
});
|
||||
|
||||
// Page 1 remains cached; the connector should skip fetching it when page 0 is unchanged.
|
||||
}
|
||||
|
||||
private static string ReadFixture(string relativePath)
|
||||
|
||||
private static string ReadFixture(string relativePath)
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
if (!File.Exists(path))
|
||||
@@ -169,10 +137,6 @@ public sealed class UbuntuConnectorTests : IAsyncLifetime
|
||||
throw new FileNotFoundException($"Fixture '{relativePath}' not found.", path);
|
||||
}
|
||||
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
}
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Ghsa.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ghsa.Tests;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
@@ -10,18 +10,18 @@ using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Ghsa.Configuration;
|
||||
using StellaOps.Concelier.Connector.Ghsa.Internal;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ghsa.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class GhsaConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private ConnectorTestHarness? _harness;
|
||||
|
||||
public GhsaConnectorTests(MongoIntegrationFixture fixture)
|
||||
public GhsaConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using StellaOps.Concelier.Connector.Ghsa.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ghsa.Tests;
|
||||
|
||||
|
||||
@@ -3,50 +3,45 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Ics.Cisa;
|
||||
using StellaOps.Concelier.Connector.Ics.Cisa.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Ics.Cisa;
|
||||
using StellaOps.Concelier.Connector.Ics.Cisa.Configuration;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ics.Cisa.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class IcsCisaConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly CannedHttpMessageHandler _handler = new();
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class IcsCisaConnectorTests
|
||||
{
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
|
||||
public IcsCisaConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture ?? throw new ArgumentNullException(nameof(fixture));
|
||||
}
|
||||
|
||||
public IcsCisaConnectorTests(MongoIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture ?? throw new ArgumentNullException(nameof(fixture));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_EndToEnd_ProducesCanonicalAdvisories()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
RegisterResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<IcsCisaConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
_handler.AssertNoPendingResponses();
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
[Fact]
|
||||
public async Task FetchParseMap_EndToEnd_ProducesCanonicalAdvisories()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
RegisterResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<IcsCisaConnector>();
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
harness.Handler.AssertNoPendingResponses();
|
||||
|
||||
var advisoryStore = harness.ServiceProvider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, advisories.Count);
|
||||
|
||||
@@ -78,78 +73,48 @@ public sealed class IcsCisaConnectorTests : IAsyncLifetime
|
||||
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));
|
||||
var infusionRange = Assert.Single(infusionPackage.VersionRanges);
|
||||
Assert.Equal("2.1", infusionRange.RangeExpression);
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddIcsCisaConnector(options =>
|
||||
{
|
||||
options.GovDeliveryCode = "TESTCODE";
|
||||
options.TopicsEndpoint = new Uri("https://feed.test/topics.rss", UriKind.Absolute);
|
||||
options.TopicIds.Clear();
|
||||
options.TopicIds.Add("USDHSCISA_TEST");
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
options.DetailBaseUri = new Uri("https://www.cisa.gov/", UriKind.Absolute);
|
||||
options.AdditionalHosts.Add("files.cisa.gov");
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(IcsCisaOptions.HttpClientName, builder =>
|
||||
{
|
||||
builder.HttpMessageHandlerBuilderActions.Add(handlerBuilder =>
|
||||
{
|
||||
handlerBuilder.PrimaryHandler = _handler;
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private void RegisterResponses()
|
||||
{
|
||||
var feedUri = new Uri("https://feed.test/topics.rss?code=TESTCODE&format=xml&topic_id=USDHSCISA_TEST", UriKind.Absolute);
|
||||
_handler.AddResponse(feedUri, () => CreateTextResponse("IcsCisa/Fixtures/sample-feed.xml", "application/rss+xml"));
|
||||
|
||||
var icsaDetail = new Uri("https://www.cisa.gov/news-events/ics-advisories/icsa-25-123-01", UriKind.Absolute);
|
||||
_handler.AddResponse(icsaDetail, () => CreateTextResponse("IcsCisa/Fixtures/icsa-25-123-01.html", "text/html"));
|
||||
|
||||
var icsmaDetail = new Uri("https://www.cisa.gov/news-events/ics-medical-advisories/icsma-25-045-01", UriKind.Absolute);
|
||||
_handler.AddResponse(icsmaDetail, () => CreateTextResponse("IcsCisa/Fixtures/icsma-25-045-01.html", "text/html"));
|
||||
}
|
||||
Assert.Equal("2.1", infusionRange.RangeExpression);
|
||||
}
|
||||
|
||||
private async Task<ConnectorTestHarness> BuildHarnessAsync()
|
||||
{
|
||||
var initialTime = new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero);
|
||||
var harness = new ConnectorTestHarness(_fixture, initialTime, IcsCisaOptions.HttpClientName);
|
||||
await harness.EnsureServiceProviderAsync(services =>
|
||||
{
|
||||
services.AddIcsCisaConnector(options =>
|
||||
{
|
||||
options.GovDeliveryCode = "TESTCODE";
|
||||
options.TopicsEndpoint = new Uri("https://feed.test/topics.rss", UriKind.Absolute);
|
||||
options.TopicIds.Clear();
|
||||
options.TopicIds.Add("USDHSCISA_TEST");
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
options.DetailBaseUri = new Uri("https://www.cisa.gov/", UriKind.Absolute);
|
||||
options.AdditionalHosts.Add("files.cisa.gov");
|
||||
});
|
||||
});
|
||||
return harness;
|
||||
}
|
||||
|
||||
private static void RegisterResponses(CannedHttpMessageHandler handler)
|
||||
{
|
||||
var feedUri = new Uri("https://feed.test/topics.rss?code=TESTCODE&format=xml&topic_id=USDHSCISA_TEST", UriKind.Absolute);
|
||||
handler.AddResponse(feedUri, () => CreateTextResponse("IcsCisa/Fixtures/sample-feed.xml", "application/rss+xml"));
|
||||
|
||||
var icsaDetail = new Uri("https://www.cisa.gov/news-events/ics-advisories/icsa-25-123-01", UriKind.Absolute);
|
||||
handler.AddResponse(icsaDetail, () => CreateTextResponse("IcsCisa/Fixtures/icsa-25-123-01.html", "text/html"));
|
||||
|
||||
var icsmaDetail = new Uri("https://www.cisa.gov/news-events/ics-medical-advisories/icsma-25-045-01", UriKind.Absolute);
|
||||
handler.AddResponse(icsmaDetail, () => CreateTextResponse("IcsCisa/Fixtures/icsma-25-045-01.html", "text/html"));
|
||||
}
|
||||
|
||||
private static HttpResponseMessage CreateTextResponse(string relativePath, string contentType)
|
||||
{
|
||||
var fullPath = Path.Combine(AppContext.BaseDirectory, relativePath);
|
||||
var content = File.ReadAllText(fullPath);
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(content, Encoding.UTF8, contentType),
|
||||
};
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
_handler.Clear();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(content, Encoding.UTF8, contentType),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
@@ -19,23 +19,24 @@ using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Ics.Kaspersky;
|
||||
using StellaOps.Concelier.Connector.Ics.Kaspersky.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ics.Kaspersky.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class KasperskyConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
private ServiceProvider? _serviceProvider;
|
||||
|
||||
public KasperskyConnectorTests(MongoIntegrationFixture fixture)
|
||||
public KasperskyConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 10, 20, 0, 0, 0, TimeSpan.Zero));
|
||||
@@ -274,19 +275,19 @@ public sealed class KasperskyConnectorTests : IAsyncLifetime
|
||||
return;
|
||||
}
|
||||
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddKasperskyIcsConnector(opts =>
|
||||
@@ -307,12 +308,10 @@ public sealed class KasperskyConnectorTests : IAsyncLifetime
|
||||
});
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
var bootstrapper = _serviceProvider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
private Task ResetDatabaseAsync()
|
||||
=> _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
}
|
||||
|
||||
private Task ResetDatabaseAsync()
|
||||
=> _fixture.TruncateAllTablesAsync();
|
||||
|
||||
private static string ReadFixture(string filename)
|
||||
{
|
||||
|
||||
@@ -6,41 +6,41 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Jvn;
|
||||
using StellaOps.Concelier.Connector.Jvn.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.JpFlags;
|
||||
using Xunit.Abstractions;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
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.Storage.Postgres;
|
||||
using Xunit.Abstractions;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Jvn.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class JvnConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private const string VulnId = "JVNDB-2024-123456";
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
private ServiceProvider? _serviceProvider;
|
||||
|
||||
public JvnConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
|
||||
public JvnConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_output = output;
|
||||
@@ -77,66 +77,28 @@ public sealed class JvnConnectorTests : IAsyncLifetime
|
||||
|
||||
var stateAfterFetch = await provider.GetRequiredService<ISourceStateRepository>()
|
||||
.TryGetAsync(JvnConnectorPlugin.SourceName, CancellationToken.None);
|
||||
if (stateAfterFetch?.Cursor is not null)
|
||||
{
|
||||
_output.WriteLine($"Fetch state cursor: {stateAfterFetch.Cursor.ToJson()}");
|
||||
}
|
||||
|
||||
var rawDocuments = await _fixture.Database
|
||||
.GetCollection<BsonDocument>("document")
|
||||
.Find(Builders<BsonDocument>.Filter.Empty)
|
||||
.ToListAsync(CancellationToken.None);
|
||||
_output.WriteLine($"Fixture document count: {rawDocuments.Count}");
|
||||
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
if (stateAfterFetch?.Cursor is not null)
|
||||
{
|
||||
_output.WriteLine($"Fetch state cursor: {stateAfterFetch.Cursor.ToJson()}");
|
||||
}
|
||||
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
|
||||
var stateAfterParse = await provider.GetRequiredService<ISourceStateRepository>()
|
||||
.TryGetAsync(JvnConnectorPlugin.SourceName, CancellationToken.None);
|
||||
_output.WriteLine($"Parse state failure reason: {stateAfterParse?.LastFailureReason ?? "<none>"}");
|
||||
if (stateAfterParse?.Cursor is not null)
|
||||
{
|
||||
_output.WriteLine($"Parse state cursor: {stateAfterParse.Cursor.ToJson()}");
|
||||
}
|
||||
|
||||
var dtoCollection = provider.GetRequiredService<IMongoDatabase>()
|
||||
.GetCollection<BsonDocument>("dto");
|
||||
var dtoDocs = await dtoCollection.Find(FilterDefinition<BsonDocument>.Empty).ToListAsync(CancellationToken.None);
|
||||
_output.WriteLine($"DTO document count: {dtoDocs.Count}");
|
||||
|
||||
var documentsAfterParse = await _fixture.Database
|
||||
.GetCollection<BsonDocument>("document")
|
||||
.Find(Builders<BsonDocument>.Filter.Empty)
|
||||
.ToListAsync(CancellationToken.None);
|
||||
_output.WriteLine($"Document statuses after parse: {string.Join(",", documentsAfterParse.Select(d => d.GetValue("status", BsonValue.Create("<missing>")).AsString))}");
|
||||
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var rawAdvisories = await _fixture.Database
|
||||
.GetCollection<BsonDocument>("advisory")
|
||||
.Find(Builders<BsonDocument>.Filter.Empty)
|
||||
.ToListAsync(CancellationToken.None);
|
||||
_output.WriteLine($"Fixture advisory count: {rawAdvisories.Count}");
|
||||
Assert.NotEmpty(rawAdvisories);
|
||||
|
||||
var providerDatabase = provider.GetRequiredService<IMongoDatabase>();
|
||||
var providerCount = await providerDatabase
|
||||
.GetCollection<BsonDocument>("advisory")
|
||||
.CountDocumentsAsync(FilterDefinition<BsonDocument>.Empty, cancellationToken: CancellationToken.None);
|
||||
_output.WriteLine($"Provider advisory count: {providerCount}");
|
||||
Assert.True(providerCount > 0, $"Provider DB advisory count was {providerCount}");
|
||||
|
||||
var typedDocs = await providerDatabase
|
||||
.GetCollection<AdvisoryDocument>("advisory")
|
||||
.Find(FilterDefinition<AdvisoryDocument>.Empty)
|
||||
.ToListAsync(CancellationToken.None);
|
||||
_output.WriteLine($"Typed advisory docs: {typedDocs.Count}");
|
||||
Assert.NotEmpty(typedDocs);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var singleAdvisory = await advisoryStore.FindAsync(VulnId, CancellationToken.None);
|
||||
Assert.NotNull(singleAdvisory);
|
||||
_output.WriteLine($"singleAdvisory null? {singleAdvisory is null}");
|
||||
if (stateAfterParse?.Cursor is not null)
|
||||
{
|
||||
_output.WriteLine($"Parse state cursor: {stateAfterParse.Cursor.ToJson()}");
|
||||
}
|
||||
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var singleAdvisory = await advisoryStore.FindAsync(VulnId, CancellationToken.None);
|
||||
Assert.NotNull(singleAdvisory);
|
||||
_output.WriteLine($"singleAdvisory null? {singleAdvisory is null}");
|
||||
|
||||
var canonical = SnapshotSerializer.ToSnapshot(singleAdvisory!).Replace("\r\n", "\n");
|
||||
var expected = ReadFixture("expected-advisory.json").Replace("\r\n", "\n");
|
||||
@@ -174,19 +136,19 @@ public sealed class JvnConnectorTests : IAsyncLifetime
|
||||
return;
|
||||
}
|
||||
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddJvnConnector(opts =>
|
||||
@@ -205,14 +167,12 @@ public sealed class JvnConnectorTests : IAsyncLifetime
|
||||
builder.PrimaryHandler = _handler;
|
||||
});
|
||||
});
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
var bootstrapper = _serviceProvider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
private Task ResetDatabaseAsync()
|
||||
=> _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private Task ResetDatabaseAsync()
|
||||
=> _fixture.TruncateAllTablesAsync();
|
||||
|
||||
private static Uri BuildOverviewUri(JvnOptions options, DateTimeOffset windowStart, DateTimeOffset windowEnd, int startItem)
|
||||
{
|
||||
|
||||
@@ -2,37 +2,37 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Kev;
|
||||
using StellaOps.Concelier.Connector.Kev.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Kev;
|
||||
using StellaOps.Concelier.Connector.Kev.Configuration;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kev.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class KevConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private static readonly Uri FeedUri = new("https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json");
|
||||
private const string CatalogEtag = "\"kev-2025-10-09\"";
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
|
||||
public KevConnectorTests(MongoIntegrationFixture fixture)
|
||||
public KevConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 10, 0, 0, 0, TimeSpan.Zero));
|
||||
@@ -77,39 +77,36 @@ public sealed class KevConnectorTests : IAsyncLifetime
|
||||
Assert.True(IsEmptyArray(state.Cursor, "pendingMappings"));
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddKevConnector(options =>
|
||||
{
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync(CancellationToken.None);
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddKevConnector(options =>
|
||||
{
|
||||
options.FeedUri = FeedUri;
|
||||
options.RequestTimeout = TimeSpan.FromSeconds(10);
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(KevOptions.HttpClientName, builderOptions =>
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler);
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler);
|
||||
});
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private void SeedCatalogResponse()
|
||||
{
|
||||
@@ -211,8 +208,8 @@ public sealed class KevConnectorTests : IAsyncLifetime
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
}
|
||||
}
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,34 +14,35 @@ using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Kisa.Configuration;
|
||||
using StellaOps.Concelier.Connector.Kisa.Internal;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Kisa.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private static readonly Uri FeedUri = new("https://test.local/rss/securityInfo.do");
|
||||
private static readonly Uri DetailPageUri = new("https://test.local/detailDos.do?IDX=5868");
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public KisaConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
|
||||
public KisaConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
@@ -373,18 +374,18 @@ public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
@@ -406,10 +407,7 @@ public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private void SeedResponses(string? versionOverride = null)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Nvd.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Nvd.Tests;
|
||||
|
||||
|
||||
@@ -4,25 +4,25 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Nvd;
|
||||
using StellaOps.Concelier.Connector.Nvd.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using System.Net;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Nvd.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class NvdConnectorHarnessTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ConnectorTestHarness _harness;
|
||||
|
||||
public NvdConnectorHarnessTests(MongoIntegrationFixture fixture)
|
||||
public NvdConnectorHarnessTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_harness = new ConnectorTestHarness(fixture, new DateTimeOffset(2024, 1, 2, 12, 0, 0, TimeSpan.Zero), NvdOptions.HttpClientName);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
@@ -20,25 +20,26 @@ using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Nvd;
|
||||
using StellaOps.Concelier.Connector.Nvd.Configuration;
|
||||
using StellaOps.Concelier.Connector.Nvd.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.ChangeHistory;
|
||||
using StellaOps.Concelier.Testing;
|
||||
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.Storage.Postgres;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Nvd.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class NvdConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private FakeTimeProvider _timeProvider;
|
||||
private readonly DateTimeOffset _initialNow;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
private ServiceProvider? _serviceProvider;
|
||||
|
||||
public NvdConnectorTests(MongoIntegrationFixture fixture)
|
||||
public NvdConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_initialNow = new DateTimeOffset(2024, 1, 2, 12, 0, 0, TimeSpan.Zero);
|
||||
@@ -511,12 +512,12 @@ public sealed class NvdConnectorTests : IAsyncLifetime
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(handler);
|
||||
|
||||
services.AddMongoStorage(storageOptions =>
|
||||
{
|
||||
storageOptions.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
storageOptions.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
storageOptions.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
services.AddConcelierPostgresStorage(storageOptions =>
|
||||
{
|
||||
storageOptions.ConnectionString = _fixture.ConnectionString;
|
||||
storageOptions.SchemaName = _fixture.SchemaName;
|
||||
storageOptions.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddNvdConnector(configure: opts =>
|
||||
@@ -535,11 +536,8 @@ public sealed class NvdConnectorTests : IAsyncLifetime
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private async Task ResetDatabaseInternalAsync()
|
||||
{
|
||||
@@ -557,10 +555,10 @@ public sealed class NvdConnectorTests : IAsyncLifetime
|
||||
_serviceProvider = null;
|
||||
}
|
||||
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
_timeProvider = new FakeTimeProvider(_initialNow);
|
||||
}
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
_handler.Clear();
|
||||
_timeProvider = new FakeTimeProvider(_initialNow);
|
||||
}
|
||||
|
||||
private sealed class MetricCollector : IDisposable
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Text.Json;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Osv.Tests;
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Osv;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Reflection;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Osv;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Normalization.Identifiers;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Osv.Tests;
|
||||
|
||||
@@ -2,12 +2,12 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Osv;
|
||||
using StellaOps.Concelier.Connector.Osv.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
@@ -13,18 +13,16 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Ru.Bdu;
|
||||
using StellaOps.Concelier.Connector.Ru.Bdu.Configuration;
|
||||
using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using Xunit;
|
||||
@@ -32,16 +30,16 @@ using Xunit.Sdk;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ru.Bdu.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class RuBduConnectorSnapshotTests : IAsyncLifetime
|
||||
{
|
||||
private const string UpdateFixturesVariable = "UPDATE_BDU_FIXTURES";
|
||||
private static readonly Uri ArchiveUri = new("https://bdu.fstec.ru/files/documents/vulxml.zip");
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private ConnectorTestHarness? _harness;
|
||||
|
||||
public RuBduConnectorSnapshotTests(MongoIntegrationFixture fixture)
|
||||
public RuBduConnectorSnapshotTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
@@ -65,10 +63,6 @@ public sealed class RuBduConnectorSnapshotTests : IAsyncLifetime
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var documentsCollection = _fixture.Database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.Document);
|
||||
var documentCount = await documentsCollection.CountDocumentsAsync(Builders<BsonDocument>.Filter.Empty);
|
||||
Assert.True(documentCount > 0, "Expected persisted documents after map stage");
|
||||
|
||||
var documentsSnapshot = await BuildDocumentsSnapshotAsync(harness.ServiceProvider, expectedDocumentIds);
|
||||
WriteOrAssertSnapshot(documentsSnapshot, "ru-bdu-documents.snapshot.json");
|
||||
|
||||
@@ -122,7 +116,7 @@ public sealed class RuBduConnectorSnapshotTests : IAsyncLifetime
|
||||
options.DataArchivePath = "files/documents/vulxml.zip";
|
||||
options.MaxVulnerabilitiesPerFetch = 25;
|
||||
options.RequestTimeout = TimeSpan.FromSeconds(30);
|
||||
var cacheRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", _fixture.Database.DatabaseNamespace.DatabaseName, "ru-bdu");
|
||||
var cacheRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", _fixture.SchemaName, "ru-bdu");
|
||||
Directory.CreateDirectory(cacheRoot);
|
||||
options.CacheDirectory = cacheRoot;
|
||||
});
|
||||
@@ -160,15 +154,7 @@ public sealed class RuBduConnectorSnapshotTests : IAsyncLifetime
|
||||
var record = await documentStore.FindAsync(documentId, CancellationToken.None);
|
||||
if (record is null)
|
||||
{
|
||||
var existing = await _fixture.Database
|
||||
.GetCollection<BsonDocument>("documents")
|
||||
.Find(Builders<BsonDocument>.Filter.Empty)
|
||||
.Project(Builders<BsonDocument>.Projection.Include("Uri"))
|
||||
.ToListAsync(CancellationToken.None);
|
||||
var uris = existing
|
||||
.Select(document => document.GetValue("Uri", BsonValue.Create(string.Empty)).AsString)
|
||||
.ToArray();
|
||||
throw new XunitException($"Document id not found: {documentId}. Known URIs: {string.Join(", ", uris)}");
|
||||
throw new XunitException($"Document id not found: {documentId}");
|
||||
}
|
||||
|
||||
records.Add(new
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Collections.Immutable;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ru.Bdu.Tests;
|
||||
|
||||
@@ -13,24 +13,24 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Ru.Nkcki;
|
||||
using StellaOps.Concelier.Connector.Ru.Nkcki.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using Xunit;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ru.Nkcki.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class RuNkckiConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private static readonly Uri ListingUri = new("https://cert.gov.ru/materialy/uyazvimosti/");
|
||||
@@ -38,11 +38,11 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
|
||||
private static readonly Uri BulletinUri = new("https://cert.gov.ru/materialy/uyazvimosti/bulletin-sample.json.zip");
|
||||
private static readonly Uri LegacyBulletinUri = new("https://cert.gov.ru/materialy/uyazvimosti/bulletin-legacy.json.zip");
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
|
||||
public RuNkckiConnectorTests(MongoIntegrationFixture fixture)
|
||||
public RuNkckiConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero));
|
||||
@@ -114,22 +114,22 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
|
||||
_handler.AssertNoPendingResponses();
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
|
||||
_handler.Clear();
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddStellaOpsCrypto();
|
||||
services.AddSourceCommon();
|
||||
@@ -137,26 +137,23 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
|
||||
{
|
||||
options.BaseAddress = new Uri("https://cert.gov.ru/");
|
||||
options.ListingPath = "/materialy/uyazvimosti/";
|
||||
options.MaxBulletinsPerFetch = 2;
|
||||
options.MaxListingPagesPerFetch = 2;
|
||||
options.MaxVulnerabilitiesPerFetch = 50;
|
||||
options.ListingCacheDuration = TimeSpan.Zero;
|
||||
var cacheRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", _fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
Directory.CreateDirectory(cacheRoot);
|
||||
options.CacheDirectory = Path.Combine(cacheRoot, "ru-nkcki");
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
});
|
||||
options.MaxBulletinsPerFetch = 2;
|
||||
options.MaxListingPagesPerFetch = 2;
|
||||
options.MaxVulnerabilitiesPerFetch = 50;
|
||||
options.ListingCacheDuration = TimeSpan.Zero;
|
||||
var cacheRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", _fixture.SchemaName);
|
||||
Directory.CreateDirectory(cacheRoot);
|
||||
options.CacheDirectory = Path.Combine(cacheRoot, "ru-nkcki");
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(RuNkckiOptions.HttpClientName, builderOptions =>
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler);
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler);
|
||||
});
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private void SeedListingAndBulletin()
|
||||
{
|
||||
@@ -286,8 +283,8 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
|
||||
throw new InvalidOperationException("Unable to locate project root for Ru.Nkcki tests.");
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task DisposeAsync()
|
||||
=> await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
}
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task DisposeAsync()
|
||||
=> await _fixture.TruncateAllTablesAsync();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Collections.Immutable;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Ru.Nkcki.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
using System.Reflection;
|
||||
|
||||
|
||||
@@ -12,16 +12,17 @@ using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.StellaOpsMirror.Internal;
|
||||
using StellaOps.Concelier.Connector.StellaOpsMirror.Settings;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
@@ -30,13 +31,13 @@ using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.StellaOpsMirror.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
|
||||
public StellaOpsMirrorConnectorTests(MongoIntegrationFixture fixture)
|
||||
public StellaOpsMirrorConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
@@ -273,7 +274,7 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync(Action<StellaOpsMirrorConnectorOptions>? configureOptions = null)
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
@@ -281,11 +282,11 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
|
||||
services.AddSingleton(_handler);
|
||||
services.AddSingleton(TimeProvider.System);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddStellaOpsCrypto();
|
||||
@@ -315,10 +316,7 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private void SeedResponses(string indexJson, string manifestContent, string bundleContent, string? signature)
|
||||
|
||||
@@ -11,33 +11,33 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Vndr.Adobe;
|
||||
using StellaOps.Concelier.Connector.Vndr.Adobe.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.PsirtFlags;
|
||||
using StellaOps.Concelier.Testing;
|
||||
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.Storage.Postgres;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Vndr.Adobe.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class AdobeConnectorFetchTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
|
||||
public AdobeConnectorFetchTests(MongoIntegrationFixture fixture)
|
||||
public AdobeConnectorFetchTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 9, 10, 0, 0, 0, TimeSpan.Zero));
|
||||
@@ -259,12 +259,10 @@ public sealed class AdobeConnectorFetchTests : IAsyncLifetime
|
||||
|
||||
Assert.Equal(normalizedExpected, normalizedSnapshot);
|
||||
|
||||
var flagsCollection = _fixture.Database.GetCollection<BsonDocument>("psirt_flags");
|
||||
var rawFlags = await flagsCollection.Find(Builders<BsonDocument>.Filter.Empty).ToListAsync();
|
||||
Assert.NotEmpty(rawFlags);
|
||||
|
||||
var flagRecord = rawFlags.Single(doc => doc["_id"].AsString == "APSB25-87");
|
||||
Assert.Equal("Adobe", flagRecord["vendor"].AsString);
|
||||
var psirtStore = provider.GetRequiredService<IPsirtFlagStore>();
|
||||
var flagRecord = await psirtStore.FindAsync("APSB25-87", CancellationToken.None);
|
||||
Assert.NotNull(flagRecord);
|
||||
Assert.Equal("Adobe", flagRecord!.Vendor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -321,21 +319,21 @@ public sealed class AdobeConnectorFetchTests : IAsyncLifetime
|
||||
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMap) && pendingMap.AsBsonArray.Count == 0);
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync(CannedHttpMessageHandler handler)
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync(CannedHttpMessageHandler handler)
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddAdobeConnector(opts =>
|
||||
@@ -353,11 +351,8 @@ public sealed class AdobeConnectorFetchTests : IAsyncLifetime
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static void SeedIndex(CannedHttpMessageHandler handler)
|
||||
{
|
||||
|
||||
@@ -15,24 +15,25 @@ using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Common.Packages;
|
||||
using StellaOps.Concelier.Connector.Vndr.Apple;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.PsirtFlags;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.PsirtFlags;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Vndr.Apple.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class AppleConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private static readonly Uri IndexUri = new("https://support.example.com/index.json");
|
||||
private static readonly Uri DetailBaseUri = new("https://support.example.com/en-us/");
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
|
||||
public AppleConnectorTests(MongoIntegrationFixture fixture)
|
||||
public AppleConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 10, 0, 0, 0, TimeSpan.Zero));
|
||||
@@ -178,21 +179,21 @@ public sealed class AppleConnectorTests : IAsyncLifetime
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync(CannedHttpMessageHandler handler)
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync(CannedHttpMessageHandler handler)
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddAppleConnector(opts =>
|
||||
@@ -213,11 +214,8 @@ public sealed class AppleConnectorTests : IAsyncLifetime
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static void SeedIndex(CannedHttpMessageHandler handler)
|
||||
{
|
||||
|
||||
@@ -5,34 +5,34 @@ using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Json;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Json;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Vndr.Chromium;
|
||||
using StellaOps.Concelier.Connector.Vndr.Chromium.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.PsirtFlags;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.PsirtFlags;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Vndr.Chromium.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class ChromiumConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly List<string> _allocatedDatabases = new();
|
||||
|
||||
public ChromiumConnectorTests(MongoIntegrationFixture fixture)
|
||||
public ChromiumConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 9, 10, 18, 0, 0, TimeSpan.Zero));
|
||||
@@ -263,19 +263,19 @@ public sealed class ChromiumConnectorTests : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync(CannedHttpMessageHandler handler, string databaseName)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = databaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync(CannedHttpMessageHandler handler, string databaseName)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(handler);
|
||||
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddChromiumConnector(opts =>
|
||||
@@ -295,13 +295,8 @@ public sealed class ChromiumConnectorTests : IAsyncLifetime
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
|
||||
return provider;
|
||||
}
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private string AllocateDatabaseName()
|
||||
{
|
||||
@@ -310,16 +305,10 @@ public sealed class ChromiumConnectorTests : IAsyncLifetime
|
||||
return name;
|
||||
}
|
||||
|
||||
private async Task DropDatabaseAsync(string databaseName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(databaseName);
|
||||
}
|
||||
catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound")
|
||||
{
|
||||
}
|
||||
}
|
||||
private async Task DropDatabaseAsync(string databaseName)
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
}
|
||||
|
||||
private static void SeedHttpFixtures(CannedHttpMessageHandler handler)
|
||||
{
|
||||
|
||||
@@ -2,13 +2,13 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Vndr.Cisco;
|
||||
using StellaOps.Concelier.Connector.Vndr.Cisco.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Vndr.Cisco.Tests;
|
||||
|
||||
@@ -10,33 +10,34 @@ using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Vndr.Msrc.Configuration;
|
||||
using StellaOps.Concelier.Connector.Vndr.Msrc.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Vndr.Msrc.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class MsrcConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private static readonly Uri TokenUri = new("https://login.microsoftonline.com/11111111-1111-1111-1111-111111111111/oauth2/v2.0/token");
|
||||
private static readonly Uri SummaryUri = new("https://api.msrc.microsoft.com/sug/v2.0/vulnerabilities");
|
||||
private static readonly Uri DetailUri = new("https://api.msrc.microsoft.com/sug/v2.0/vulnerability/ADV123456");
|
||||
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
|
||||
public MsrcConnectorTests(MongoIntegrationFixture fixture)
|
||||
public MsrcConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
@@ -80,22 +81,22 @@ public sealed class MsrcConnectorTests : IAsyncLifetime
|
||||
cvrfDocument!.Status.Should().Be(DocumentStatuses.Mapped);
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton(_handler);
|
||||
services.AddSingleton(TimeProvider.System);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddMsrcConnector(options =>
|
||||
@@ -126,11 +127,8 @@ public sealed class MsrcConnectorTests : IAsyncLifetime
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private void SeedResponses()
|
||||
{
|
||||
|
||||
@@ -10,32 +10,33 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Vndr.Oracle;
|
||||
using StellaOps.Concelier.Connector.Vndr.Oracle.Configuration;
|
||||
using StellaOps.Concelier.Connector.Vndr.Oracle.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit.Abstractions;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Storage.PsirtFlags;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Vndr.Oracle.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class OracleConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
private readonly ITestOutputHelper _output;
|
||||
@@ -44,7 +45,7 @@ public sealed class OracleConnectorTests : IAsyncLifetime
|
||||
private static readonly Uri AdvisoryTwo = new("https://www.oracle.com/security-alerts/cpuapr2024-02.html");
|
||||
private static readonly Uri CalendarUri = new("https://www.oracle.com/security-alerts/cpuapr2024.html");
|
||||
|
||||
public OracleConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
|
||||
public OracleConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 4, 18, 0, 0, 0, TimeSpan.Zero));
|
||||
@@ -105,11 +106,19 @@ public sealed class OracleConnectorTests : IAsyncLifetime
|
||||
|
||||
Assert.Equal(normalizedExpected, normalizedSnapshot);
|
||||
|
||||
var psirtCollection = _fixture.Database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.PsirtFlags);
|
||||
var flags = await psirtCollection.Find(Builders<BsonDocument>.Filter.Empty).ToListAsync();
|
||||
_output.WriteLine("Psirt flags: " + string.Join(", ", flags.Select(doc => doc.GetValue("_id", BsonValue.Create("<missing>")).ToString())));
|
||||
Assert.Equal(2, flags.Count);
|
||||
Assert.All(flags, doc => Assert.Equal("Oracle", doc["vendor"].AsString));
|
||||
var psirtStore = provider.GetRequiredService<IPsirtFlagStore>();
|
||||
var flags = new List<PsirtFlagRecord>();
|
||||
foreach (var advisory in advisories)
|
||||
{
|
||||
var flag = await psirtStore.FindAsync(advisory.AdvisoryKey, CancellationToken.None);
|
||||
if (flag is not null)
|
||||
{
|
||||
flags.Add(flag);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(2, flags.Count);
|
||||
Assert.All(flags, flag => Assert.Equal("Oracle", flag.Vendor));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -149,9 +158,11 @@ public sealed class OracleConnectorTests : IAsyncLifetime
|
||||
Assert.NotNull(second);
|
||||
Assert.Equal(DocumentStatuses.Mapped, second!.Status);
|
||||
|
||||
var dtoCollection = _fixture.Database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.Dto);
|
||||
var dtoCount = await dtoCollection.CountDocumentsAsync(Builders<BsonDocument>.Filter.Empty);
|
||||
Assert.Equal(2, dtoCount);
|
||||
var dtoStore = provider.GetRequiredService<IDtoStore>();
|
||||
var dto1 = await dtoStore.FindByDocumentIdAsync(first!.Id, CancellationToken.None);
|
||||
Assert.NotNull(dto1);
|
||||
var dto2 = await dtoStore.FindByDocumentIdAsync(second!.Id, CancellationToken.None);
|
||||
Assert.NotNull(dto2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -209,18 +220,10 @@ public sealed class OracleConnectorTests : IAsyncLifetime
|
||||
Assert.NotNull(invalidDocument);
|
||||
_output.WriteLine($"Invalid document status: {invalidDocument!.Status}");
|
||||
|
||||
var rawDoc = await _fixture.Database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.Document)
|
||||
.Find(Builders<BsonDocument>.Filter.Eq("uri", AdvisoryOne.ToString()))
|
||||
.FirstOrDefaultAsync();
|
||||
if (rawDoc is not null)
|
||||
{
|
||||
_output.WriteLine("Raw document: " + rawDoc.ToJson());
|
||||
}
|
||||
|
||||
var dtoStore = provider.GetRequiredService<IDtoStore>();
|
||||
var invalidDto = await dtoStore.FindByDocumentIdAsync(invalidDocument.Id, CancellationToken.None);
|
||||
if (invalidDto is not null)
|
||||
{
|
||||
var dtoStore = provider.GetRequiredService<IDtoStore>();
|
||||
var invalidDto = await dtoStore.FindByDocumentIdAsync(invalidDocument.Id, CancellationToken.None);
|
||||
if (invalidDto is not null)
|
||||
{
|
||||
_output.WriteLine("Validation unexpectedly succeeded. DTO: " + invalidDto.Payload.ToJson());
|
||||
}
|
||||
Assert.Equal(DocumentStatuses.Failed, invalidDocument.Status);
|
||||
@@ -234,12 +237,15 @@ public sealed class OracleConnectorTests : IAsyncLifetime
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
var advisories = await provider.GetRequiredService<IAdvisoryStore>().GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Single(advisories);
|
||||
Assert.Equal("oracle/cpuapr2024-02-html", advisories[0].AdvisoryKey);
|
||||
|
||||
var psirtCollection = _fixture.Database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.PsirtFlags);
|
||||
var flagCount = await psirtCollection.CountDocumentsAsync(Builders<BsonDocument>.Filter.Empty);
|
||||
Assert.Equal(1, flagCount);
|
||||
Assert.Single(advisories);
|
||||
Assert.Equal("oracle/cpuapr2024-02-html", advisories[0].AdvisoryKey);
|
||||
|
||||
var psirtStore = provider.GetRequiredService<IPsirtFlagStore>();
|
||||
var validFlag = await psirtStore.FindAsync(advisories[0].AdvisoryKey, CancellationToken.None);
|
||||
Assert.NotNull(validFlag);
|
||||
|
||||
var missingFlag = await psirtStore.FindAsync("oracle/cpuapr2024-01-html", CancellationToken.None);
|
||||
Assert.Null(missingFlag);
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(VndrOracleConnectorPlugin.SourceName, CancellationToken.None);
|
||||
@@ -249,22 +255,22 @@ public sealed class OracleConnectorTests : IAsyncLifetime
|
||||
Assert.Empty(cursor.PendingMappings);
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddOracleConnector(opts =>
|
||||
@@ -281,11 +287,8 @@ public sealed class OracleConnectorTests : IAsyncLifetime
|
||||
});
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private void SeedDetails()
|
||||
{
|
||||
|
||||
@@ -9,32 +9,33 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Vndr.Vmware;
|
||||
using StellaOps.Concelier.Connector.Vndr.Vmware.Configuration;
|
||||
using StellaOps.Concelier.Connector.Vndr.Vmware.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit.Abstractions;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Storage.PsirtFlags;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Vndr.Vmware.Tests.Vmware;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class VmwareConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
private readonly ITestOutputHelper _output;
|
||||
@@ -44,7 +45,7 @@ public sealed class VmwareConnectorTests : IAsyncLifetime
|
||||
private static readonly Uri DetailTwo = new("https://vmware.example/api/vmsa/VMSA-2024-0002.json");
|
||||
private static readonly Uri DetailThree = new("https://vmware.example/api/vmsa/VMSA-2024-0003.json");
|
||||
|
||||
public VmwareConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
|
||||
public VmwareConnectorTests(ConcelierPostgresFixture fixture, ITestOutputHelper output)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 4, 5, 0, 0, 0, TimeSpan.Zero));
|
||||
@@ -81,11 +82,19 @@ public sealed class VmwareConnectorTests : IAsyncLifetime
|
||||
|
||||
Assert.Equal(expected, snapshot);
|
||||
|
||||
var psirtCollection = _fixture.Database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.PsirtFlags);
|
||||
var psirtFlags = await psirtCollection.Find(Builders<BsonDocument>.Filter.Empty).ToListAsync();
|
||||
_output.WriteLine("PSIRT flags after initial map: " + string.Join(", ", psirtFlags.Select(flag => flag.GetValue("_id", BsonValue.Create("<missing>")).ToString())));
|
||||
Assert.Equal(2, psirtFlags.Count);
|
||||
Assert.All(psirtFlags, doc => Assert.Equal("VMware", doc["vendor"].AsString));
|
||||
var psirtStore = provider.GetRequiredService<IPsirtFlagStore>();
|
||||
var psirtFlags = new List<PsirtFlagRecord>();
|
||||
foreach (var advisory in ordered)
|
||||
{
|
||||
var flag = await psirtStore.FindAsync(advisory.AdvisoryKey, CancellationToken.None);
|
||||
if (flag is not null)
|
||||
{
|
||||
psirtFlags.Add(flag);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(2, psirtFlags.Count);
|
||||
Assert.All(psirtFlags, flag => Assert.Equal("VMware", flag.Vendor));
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(VmwareConnectorPlugin.SourceName, CancellationToken.None);
|
||||
@@ -149,22 +158,22 @@ public sealed class VmwareConnectorTests : IAsyncLifetime
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
|
||||
_handler.Clear();
|
||||
private async Task<ServiceProvider> BuildServiceProviderAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
_handler.Clear();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton(_handler);
|
||||
|
||||
services.AddMongoStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.Runner.ConnectionString;
|
||||
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
|
||||
options.CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
});
|
||||
services.AddConcelierPostgresStorage(options =>
|
||||
{
|
||||
options.ConnectionString = _fixture.ConnectionString;
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
options.CommandTimeoutSeconds = 5;
|
||||
});
|
||||
|
||||
services.AddSourceCommon();
|
||||
services.AddVmwareConnector(opts =>
|
||||
@@ -181,11 +190,8 @@ public sealed class VmwareConnectorTests : IAsyncLifetime
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = _handler);
|
||||
});
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
return provider;
|
||||
}
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private void SeedInitialResponses()
|
||||
{
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Vndr.Vmware;
|
||||
using StellaOps.Concelier.Connector.Vndr.Vmware.Internal;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Vndr.Vmware.Tests;
|
||||
|
||||
@@ -9,8 +9,8 @@ using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
using StellaOps.Concelier.Core.Events;
|
||||
using StellaOps.Concelier.Exporter.Json;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Exporting;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Exporting;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
|
||||
@@ -16,8 +16,8 @@ using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Core.Events;
|
||||
using StellaOps.Concelier.Exporter.Json;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Exporting;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Exporting;
|
||||
using StellaOps.Provenance.Mongo;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using StellaOps.Concelier.Exporter.TrivyDb;
|
||||
using StellaOps.Concelier.Storage.Mongo.Exporting;
|
||||
using StellaOps.Concelier.Storage.Exporting;
|
||||
|
||||
namespace StellaOps.Concelier.Exporter.TrivyDb.Tests;
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Exporter.Json;
|
||||
using StellaOps.Concelier.Exporter.TrivyDb;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Exporting;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Exporting;
|
||||
|
||||
namespace StellaOps.Concelier.Exporter.TrivyDb.Tests;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Concelier.Storage.Mongo.Exporting;
|
||||
using StellaOps.Concelier.Storage.Exporting;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Exporter.TrivyDb.Tests;
|
||||
|
||||
@@ -8,9 +8,9 @@ using StellaOps.Concelier.Core;
|
||||
using StellaOps.Concelier.Core.Events;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Aliases;
|
||||
using StellaOps.Concelier.Storage.Mongo.MergeEvents;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Aliases;
|
||||
using StellaOps.Concelier.Storage.MergeEvents;
|
||||
using StellaOps.Provenance.Mongo;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
@@ -1,34 +1,20 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Aliases;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class AliasGraphResolverTests : IClassFixture<MongoIntegrationFixture>
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
|
||||
public AliasGraphResolverTests(MongoIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ReturnsCollisions_WhenAliasesOverlap()
|
||||
{
|
||||
await DropAliasCollectionAsync();
|
||||
|
||||
var aliasStore = new AliasStore(_fixture.Database, NullLogger<AliasStore>.Instance);
|
||||
var resolver = new AliasGraphResolver(aliasStore);
|
||||
|
||||
var timestamp = DateTimeOffset.UtcNow;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Storage.Aliases;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class AliasGraphResolverTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ReturnsCollisions_WhenAliasesOverlap()
|
||||
{
|
||||
var aliasStore = new AliasStore();
|
||||
var resolver = new AliasGraphResolver(aliasStore);
|
||||
|
||||
var timestamp = DateTimeOffset.UtcNow;
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-1",
|
||||
new[] { new AliasEntry("CVE", "CVE-2025-2000"), new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-1") },
|
||||
@@ -50,13 +36,12 @@ public sealed class AliasGraphResolverTests : IClassFixture<MongoIntegrationFixt
|
||||
Assert.Contains("ADV-1", collision.AdvisoryKeys);
|
||||
Assert.Contains("ADV-2", collision.AdvisoryKeys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildComponentAsync_TracesConnectedAdvisories()
|
||||
{
|
||||
await DropAliasCollectionAsync();
|
||||
var aliasStore = new AliasStore(_fixture.Database, NullLogger<AliasStore>.Instance);
|
||||
var resolver = new AliasGraphResolver(aliasStore);
|
||||
|
||||
[Fact]
|
||||
public async Task BuildComponentAsync_TracesConnectedAdvisories()
|
||||
{
|
||||
var aliasStore = new AliasStore();
|
||||
var resolver = new AliasGraphResolver(aliasStore);
|
||||
|
||||
var timestamp = DateTimeOffset.UtcNow;
|
||||
await aliasStore.ReplaceAsync(
|
||||
@@ -83,28 +68,15 @@ public sealed class AliasGraphResolverTests : IClassFixture<MongoIntegrationFixt
|
||||
Assert.Contains("ADV-C", component.AdvisoryKeys, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.NotEmpty(component.Collisions);
|
||||
Assert.True(component.AliasMap.ContainsKey("ADV-A"));
|
||||
Assert.Contains(component.AliasMap["ADV-B"], record => record.Scheme == "OSV" && record.Value == "OSV-2025-1");
|
||||
}
|
||||
|
||||
private async Task DropAliasCollectionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _fixture.Database.DropCollectionAsync(MongoStorageDefaults.Collections.Alias);
|
||||
}
|
||||
catch (MongoDB.Driver.MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildComponentAsync_LinksOsvAndGhsaAliases()
|
||||
{
|
||||
await DropAliasCollectionAsync();
|
||||
|
||||
var aliasStore = new AliasStore(_fixture.Database, NullLogger<AliasStore>.Instance);
|
||||
var resolver = new AliasGraphResolver(aliasStore);
|
||||
var timestamp = DateTimeOffset.UtcNow;
|
||||
Assert.Contains(component.AliasMap["ADV-B"], record => record.Scheme == "OSV" && record.Value == "OSV-2025-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildComponentAsync_LinksOsvAndGhsaAliases()
|
||||
{
|
||||
var aliasStore = new AliasStore();
|
||||
var resolver = new AliasGraphResolver(aliasStore);
|
||||
var timestamp = DateTimeOffset.UtcNow;
|
||||
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-OSV",
|
||||
|
||||
@@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo.MergeEvents;
|
||||
using StellaOps.Concelier.Storage.MergeEvents;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.MergeEvents;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class MergePrecedenceIntegrationTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private MergeEventStore? _mergeEventStore;
|
||||
private MergeEventWriter? _mergeEventWriter;
|
||||
private AdvisoryPrecedenceMerger? _merger;
|
||||
private FakeTimeProvider? _timeProvider;
|
||||
|
||||
public MergePrecedenceIntegrationTests(MongoIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.MergeEvents;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class MergePrecedenceIntegrationTests : IAsyncLifetime
|
||||
{
|
||||
private MergeEventStore? _mergeEventStore;
|
||||
private MergeEventWriter? _mergeEventWriter;
|
||||
private AdvisoryPrecedenceMerger? _merger;
|
||||
private FakeTimeProvider? _timeProvider;
|
||||
|
||||
[Fact]
|
||||
public async Task MergePipeline_PsirtOverridesNvd_AndKevOnlyTogglesExploitKnown()
|
||||
@@ -82,29 +72,28 @@ public sealed class MergePrecedenceIntegrationTests : IAsyncLifetime
|
||||
AutoAdvanceAmount = TimeSpan.Zero,
|
||||
};
|
||||
_merger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), _timeProvider);
|
||||
_mergeEventStore = new MergeEventStore(_fixture.Database, NullLogger<MergeEventStore>.Instance);
|
||||
_mergeEventWriter = new MergeEventWriter(_mergeEventStore, new CanonicalHashCalculator(), _timeProvider, NullLogger<MergeEventWriter>.Instance);
|
||||
await DropMergeCollectionAsync();
|
||||
}
|
||||
_mergeEventStore = new MergeEventStore();
|
||||
_mergeEventWriter = new MergeEventWriter(_mergeEventStore, new CanonicalHashCalculator(), _timeProvider, NullLogger<MergeEventWriter>.Instance);
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
private async Task EnsureInitializedAsync()
|
||||
{
|
||||
if (_mergeEventWriter is null)
|
||||
{
|
||||
await InitializeAsync();
|
||||
}
|
||||
}
|
||||
private async Task EnsureInitializedAsync()
|
||||
{
|
||||
if (_mergeEventWriter is null)
|
||||
{
|
||||
await InitializeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DropMergeCollectionAsync()
|
||||
private Task DropMergeCollectionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _fixture.Database.DropCollectionAsync(MongoStorageDefaults.Collections.MergeEvent);
|
||||
}
|
||||
catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
// {
|
||||
// await _fixture.Database.DropCollectionAsync(MongoStorageDefaults.Collections.MergeEvent);
|
||||
// }
|
||||
// catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase))
|
||||
// {
|
||||
// Collection has not been created yet – safe to ignore.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Storage.Postgres.Converters;
|
||||
using StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using FluentAssertions;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Postgres.Converters;
|
||||
using Xunit;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Storage.Postgres.Converters;
|
||||
using StellaOps.Concelier.Storage.Postgres.Converters.Importers;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Storage.Postgres.Converters;
|
||||
using StellaOps.Concelier.Storage.Postgres.Converters.Importers;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Storage.Postgres.Converters;
|
||||
using StellaOps.Concelier.Storage.Postgres.Converters.Importers;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Driver.Linq;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Storage.Postgres.Converters;
|
||||
using StellaOps.Concelier.Storage.Postgres.Converters.Importers;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Storage.Postgres.Converters;
|
||||
using StellaOps.Concelier.Storage.Postgres.Converters.Importers;
|
||||
|
||||
@@ -2,9 +2,9 @@ using System.Reflection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Aliases;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Aliases;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Storage.Postgres.Advisories;
|
||||
using StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
@@ -26,7 +26,7 @@ namespace StellaOps.Concelier.Storage.Postgres.Tests.Parity;
|
||||
/// </remarks>
|
||||
public sealed class DualBackendFixture : IAsyncLifetime
|
||||
{
|
||||
private MongoIntegrationFixture? _mongoFixture;
|
||||
private ConcelierPostgresFixture? _mongoFixture;
|
||||
private PostgreSqlContainer? _postgresContainer;
|
||||
private PostgresFixture? _postgresFixture;
|
||||
|
||||
@@ -53,7 +53,7 @@ public sealed class DualBackendFixture : IAsyncLifetime
|
||||
/// <summary>
|
||||
/// Gets the MongoDB integration fixture for test cleanup.
|
||||
/// </summary>
|
||||
public MongoIntegrationFixture MongoFixture => _mongoFixture
|
||||
public ConcelierPostgresFixture MongoFixture => _mongoFixture
|
||||
?? throw new InvalidOperationException("MongoDB fixture not initialized");
|
||||
|
||||
/// <summary>
|
||||
@@ -71,7 +71,7 @@ public sealed class DualBackendFixture : IAsyncLifetime
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// Initialize MongoDB
|
||||
_mongoFixture = new MongoIntegrationFixture();
|
||||
_mongoFixture = new ConcelierPostgresFixture();
|
||||
await _mongoFixture.InitializeAsync();
|
||||
|
||||
var mongoOptions = Options.Create(new MongoStorageOptions());
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Postgres;
|
||||
using StellaOps.Concelier.Storage.Postgres.Converters;
|
||||
using StellaOps.Concelier.Storage.Postgres.Converters.Importers;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using StellaOps.Concelier.Bson.Serialization.Attributes;
|
||||
using StellaOps.Concelier.RawModels;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Tests.Fixtures;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using StellaOps.Concelier.Bson.Serialization.Attributes;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Tests;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Core.Orchestration;
|
||||
using StellaOps.Concelier.WebService;
|
||||
using StellaOps.Concelier.WebService.Options;
|
||||
|
||||
@@ -24,8 +24,8 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Mongo2Go;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.IO;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Bson.IO;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Core.Attestation;
|
||||
using static StellaOps.Concelier.WebService.Program;
|
||||
@@ -33,10 +33,10 @@ using StellaOps.Concelier.Core.Events;
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Observations;
|
||||
using StellaOps.Concelier.Storage.Mongo.Linksets;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Observations;
|
||||
using StellaOps.Concelier.Storage.Linksets;
|
||||
using StellaOps.Concelier.Core.Raw;
|
||||
using StellaOps.Concelier.WebService.Jobs;
|
||||
using StellaOps.Concelier.WebService.Options;
|
||||
|
||||
Reference in New Issue
Block a user