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
				
			
		
			
				
	
	
		
			264 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			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();
 | |
|     }
 | |
| }
 |