feat: add Reachability Center and Why Drawer components with tests
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled

- 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:
master
2025-12-12 18:50:35 +02:00
parent efaf3cb789
commit 3f3473ee3a
320 changed files with 10635 additions and 3677 deletions

View File

@@ -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\"");

View File

@@ -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);

View File

@@ -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));
}

View File

@@ -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;

View File

@@ -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));
}

View File

@@ -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()
{

View File

@@ -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;
}

View File

@@ -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\"")
{

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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));

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
using MongoDB.Bson;
using StellaOps.Concelier.Bson;
using StellaOps.Concelier.Connector.Common.Cursors;
namespace StellaOps.Concelier.Connector.Common.Tests;

View File

@@ -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;

View File

@@ -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()
{

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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),
};
}
}

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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);
}
}

View File

@@ -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)

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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
{

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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;

View File

@@ -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()
{

View File

@@ -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()
{

View File

@@ -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()
{

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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",

View File

@@ -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;

View File

@@ -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.
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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());

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;