Files
git.stella-ops.org/src/StellaOps.Feedser.Source.CertCc.Tests/CertCc/CertCcConnectorFetchTests.cs
master b97fc7685a
Some checks failed
Build Test Deploy / authority-container (push) Has been cancelled
Build Test Deploy / docs (push) Has been cancelled
Build Test Deploy / deploy (push) Has been cancelled
Build Test Deploy / build-test (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Initial commit (history squashed)
2025-10-11 23:28:35 +03:00

264 lines
9.7 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
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.Feedser.Source.CertCc;
using StellaOps.Feedser.Source.CertCc.Configuration;
using StellaOps.Feedser.Source.CertCc.Internal;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Http;
using StellaOps.Feedser.Source.Common.Cursors;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Testing;
using Xunit;
namespace StellaOps.Feedser.Source.CertCc.Tests.CertCc;
[Collection("mongo-fixture")]
public sealed class CertCcConnectorFetchTests : IAsyncLifetime
{
private const string TestNoteId = "294418";
private readonly MongoIntegrationFixture _fixture;
private readonly FakeTimeProvider _timeProvider;
private readonly CannedHttpMessageHandler _handler;
private ServiceProvider? _serviceProvider;
public CertCcConnectorFetchTests(MongoIntegrationFixture fixture)
{
_fixture = fixture;
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 11, 8, 0, 0, TimeSpan.Zero));
_handler = new CannedHttpMessageHandler();
}
[Fact(Skip = "Superseded by snapshot regression coverage (FEEDCONN-CERTCC-02-005).")]
public async Task FetchAsync_PersistsSummaryAndDetailDocumentsAndUpdatesCursor()
{
var template = new CertCcOptions
{
BaseApiUri = new Uri("https://www.kb.cert.org/vuls/api/", UriKind.Absolute),
SummaryWindow = new TimeWindowCursorOptions
{
WindowSize = TimeSpan.FromDays(30),
Overlap = TimeSpan.FromDays(5),
InitialBackfill = TimeSpan.FromDays(60),
MinimumWindowSize = TimeSpan.FromDays(1),
},
MaxMonthlySummaries = 3,
MaxNotesPerFetch = 3,
DetailRequestDelay = TimeSpan.Zero,
};
await EnsureServiceProviderAsync(template);
var provider = _serviceProvider!;
_handler.Clear();
var planner = provider.GetRequiredService<CertCcSummaryPlanner>();
var plan = planner.CreatePlan(state: null);
Assert.NotEmpty(plan.Requests);
foreach (var request in plan.Requests)
{
_handler.AddJsonResponse(request.Uri, BuildSummaryPayload());
}
RegisterDetailResponses();
var connector = provider.GetRequiredService<CertCcConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
var documentStore = provider.GetRequiredService<IDocumentStore>();
foreach (var request in plan.Requests)
{
var record = await documentStore.FindBySourceAndUriAsync(CertCcConnectorPlugin.SourceName, request.Uri.ToString(), CancellationToken.None);
Assert.NotNull(record);
Assert.Equal(DocumentStatuses.PendingParse, record!.Status);
Assert.NotNull(record.Metadata);
Assert.Equal(request.Scope.ToString().ToLowerInvariant(), record.Metadata!["certcc.scope"]);
Assert.Equal(request.Year.ToString("D4"), record.Metadata["certcc.year"]);
if (request.Month.HasValue)
{
Assert.Equal(request.Month.Value.ToString("D2"), record.Metadata["certcc.month"]);
}
else
{
Assert.False(record.Metadata.ContainsKey("certcc.month"));
}
}
foreach (var uri in EnumerateDetailUris())
{
var record = await documentStore.FindBySourceAndUriAsync(CertCcConnectorPlugin.SourceName, uri.ToString(), CancellationToken.None);
Assert.NotNull(record);
Assert.Equal(DocumentStatuses.PendingParse, record!.Status);
Assert.NotNull(record.Metadata);
Assert.Equal(TestNoteId, record.Metadata!["certcc.noteId"]);
}
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(CertCcConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
BsonValue summaryValue;
Assert.True(state!.Cursor.TryGetValue("summary", out summaryValue));
var summaryDocument = Assert.IsType<BsonDocument>(summaryValue);
Assert.True(summaryDocument.TryGetValue("start", out _));
Assert.True(summaryDocument.TryGetValue("end", out _));
var pendingNotesCount = state.Cursor.TryGetValue("pendingNotes", out var pendingNotesValue)
? pendingNotesValue.AsBsonArray.Count
: 0;
Assert.Equal(0, pendingNotesCount);
var pendingSummariesCount = state.Cursor.TryGetValue("pendingSummaries", out var pendingSummariesValue)
? pendingSummariesValue.AsBsonArray.Count
: 0;
Assert.Equal(0, pendingSummariesCount);
Assert.True(state.Cursor.TryGetValue("lastRun", out _));
Assert.True(_handler.Requests.Count >= plan.Requests.Count);
foreach (var request in _handler.Requests)
{
if (request.Headers.TryGetValue("Accept", out var accept))
{
Assert.Contains("application/json", accept, StringComparison.OrdinalIgnoreCase);
}
}
}
private static string BuildSummaryPayload()
{
return $$"""
{
"count": 1,
"notes": [
"VU#{TestNoteId}"
]
}
""";
}
private void RegisterDetailResponses()
{
foreach (var uri in EnumerateDetailUris())
{
var fixtureName = uri.AbsolutePath.EndsWith("/vendors/", StringComparison.OrdinalIgnoreCase)
? "vu-294418-vendors.json"
: uri.AbsolutePath.EndsWith("/vuls/", StringComparison.OrdinalIgnoreCase)
? "vu-294418-vuls.json"
: "vu-294418.json";
_handler.AddJsonResponse(uri, ReadFixture(fixtureName));
}
}
private static IEnumerable<Uri> EnumerateDetailUris()
{
var baseUri = new Uri("https://www.kb.cert.org/vuls/api/", UriKind.Absolute);
yield return new Uri(baseUri, $"{TestNoteId}/");
yield return new Uri(baseUri, $"{TestNoteId}/vendors/");
yield return new Uri(baseUri, $"{TestNoteId}/vuls/");
}
private async Task EnsureServiceProviderAsync(CertCcOptions template)
{
await DisposeServiceProviderAsync();
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);
options.RawDocumentRetention = TimeSpan.Zero;
options.RawDocumentRetentionTtlGrace = TimeSpan.FromMinutes(5);
options.RawDocumentRetentionSweepInterval = TimeSpan.FromHours(1);
});
services.AddSourceCommon();
services.AddCertCcConnector(options =>
{
options.BaseApiUri = template.BaseApiUri;
options.SummaryWindow = new TimeWindowCursorOptions
{
WindowSize = template.SummaryWindow.WindowSize,
Overlap = template.SummaryWindow.Overlap,
InitialBackfill = template.SummaryWindow.InitialBackfill,
MinimumWindowSize = template.SummaryWindow.MinimumWindowSize,
};
options.MaxMonthlySummaries = template.MaxMonthlySummaries;
options.MaxNotesPerFetch = template.MaxNotesPerFetch;
options.DetailRequestDelay = template.DetailRequestDelay;
options.EnableDetailMapping = template.EnableDetailMapping;
});
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);
}
private async Task DisposeServiceProviderAsync()
{
if (_serviceProvider is null)
{
return;
}
if (_serviceProvider is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
_serviceProvider.Dispose();
}
_serviceProvider = null;
}
private static string ReadFixture(string filename)
{
var baseDirectory = AppContext.BaseDirectory;
var primary = Path.Combine(baseDirectory, "Fixtures", filename);
if (File.Exists(primary))
{
return File.ReadAllText(primary);
}
return File.ReadAllText(Path.Combine(baseDirectory, filename));
}
public Task InitializeAsync()
{
_handler.Clear();
return Task.CompletedTask;
}
public async Task DisposeAsync()
{
await DisposeServiceProviderAsync();
}
}