feat: add Reachability Center and Why Drawer components with tests
- Implemented ReachabilityCenterComponent for displaying asset reachability status with summary and filtering options. - Added ReachabilityWhyDrawerComponent to show detailed reachability evidence and call paths. - Created unit tests for both components to ensure functionality and correctness. - Updated accessibility test results for the new components.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user