Add channel test providers for Email, Slack, Teams, and Webhook
- Implemented EmailChannelTestProvider to generate email preview payloads. - Implemented SlackChannelTestProvider to create Slack message previews. - Implemented TeamsChannelTestProvider for generating Teams Adaptive Card previews. - Implemented WebhookChannelTestProvider to create webhook payloads. - Added INotifyChannelTestProvider interface for channel-specific preview generation. - Created ChannelTestPreviewContracts for request and response models. - Developed NotifyChannelTestService to handle test send requests and generate previews. - Added rate limit policies for test sends and delivery history. - Implemented unit tests for service registration and binding. - Updated project files to include necessary dependencies and configurations.
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Concelier.Core.Events;
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Core.Tests.Events;
|
||||
|
||||
public sealed class AdvisoryEventLogTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AppendAsync_PersistsCanonicalStatementEntries()
|
||||
{
|
||||
var repository = new FakeRepository();
|
||||
var timeProvider = new FixedTimeProvider(DateTimeOffset.UtcNow);
|
||||
var log = new AdvisoryEventLog(repository, timeProvider);
|
||||
|
||||
var advisory = new Advisory(
|
||||
"adv-1",
|
||||
"Test Advisory",
|
||||
summary: "Summary",
|
||||
language: "en",
|
||||
published: DateTimeOffset.Parse("2025-10-01T00:00:00Z"),
|
||||
modified: DateTimeOffset.Parse("2025-10-02T00:00:00Z"),
|
||||
severity: "high",
|
||||
exploitKnown: true,
|
||||
aliases: new[] { "CVE-2025-0001" },
|
||||
references: Array.Empty<AdvisoryReference>(),
|
||||
affectedPackages: Array.Empty<AffectedPackage>(),
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance: Array.Empty<AdvisoryProvenance>());
|
||||
|
||||
var statementInput = new AdvisoryStatementInput(
|
||||
VulnerabilityKey: "CVE-2025-0001",
|
||||
Advisory: advisory,
|
||||
AsOf: DateTimeOffset.Parse("2025-10-03T00:00:00Z"),
|
||||
InputDocumentIds: new[] { Guid.Parse("11111111-1111-1111-1111-111111111111") });
|
||||
|
||||
await log.AppendAsync(new AdvisoryEventAppendRequest(new[] { statementInput }), CancellationToken.None);
|
||||
|
||||
Assert.Single(repository.InsertedStatements);
|
||||
var entry = repository.InsertedStatements.Single();
|
||||
Assert.Equal("cve-2025-0001", entry.VulnerabilityKey);
|
||||
Assert.Equal("adv-1", entry.AdvisoryKey);
|
||||
Assert.Equal(DateTimeOffset.Parse("2025-10-03T00:00:00Z"), entry.AsOf);
|
||||
Assert.Contains("\"advisoryKey\":\"adv-1\"", entry.CanonicalJson);
|
||||
Assert.NotEqual(ImmutableArray<byte>.Empty, entry.StatementHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AppendAsync_PersistsConflictsWithCanonicalizedJson()
|
||||
{
|
||||
var repository = new FakeRepository();
|
||||
var timeProvider = new FixedTimeProvider(DateTimeOffset.Parse("2025-10-19T12:00:00Z"));
|
||||
var log = new AdvisoryEventLog(repository, timeProvider);
|
||||
|
||||
using var conflictJson = JsonDocument.Parse("{\"reason\":\"tie\",\"details\":{\"b\":2,\"a\":1}}");
|
||||
var conflictInput = new AdvisoryConflictInput(
|
||||
VulnerabilityKey: "CVE-2025-0001",
|
||||
Details: conflictJson,
|
||||
AsOf: DateTimeOffset.Parse("2025-10-04T00:00:00Z"),
|
||||
StatementIds: new[] { Guid.Parse("22222222-2222-2222-2222-222222222222") });
|
||||
|
||||
await log.AppendAsync(new AdvisoryEventAppendRequest(Array.Empty<AdvisoryStatementInput>(), new[] { conflictInput }), CancellationToken.None);
|
||||
|
||||
Assert.Single(repository.InsertedConflicts);
|
||||
var entry = repository.InsertedConflicts.Single();
|
||||
Assert.Equal("cve-2025-0001", entry.VulnerabilityKey);
|
||||
Assert.Equal("{\"details\":{\"a\":1,\"b\":2},\"reason\":\"tie\"}", entry.CanonicalJson);
|
||||
Assert.NotEqual(ImmutableArray<byte>.Empty, entry.ConflictHash);
|
||||
Assert.Equal(DateTimeOffset.Parse("2025-10-04T00:00:00Z"), entry.AsOf);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReplayAsync_ReturnsSortedSnapshots()
|
||||
{
|
||||
var repository = new FakeRepository();
|
||||
var timeProvider = new FixedTimeProvider(DateTimeOffset.Parse("2025-10-05T00:00:00Z"));
|
||||
var log = new AdvisoryEventLog(repository, timeProvider);
|
||||
|
||||
repository.StoredStatements.AddRange(new[]
|
||||
{
|
||||
new AdvisoryStatementEntry(
|
||||
Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
||||
"cve-2025-0001",
|
||||
"adv-2",
|
||||
CanonicalJsonSerializer.Serialize(new Advisory(
|
||||
"adv-2",
|
||||
"B title",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
DateTimeOffset.Parse("2025-10-02T00:00:00Z"),
|
||||
null,
|
||||
false,
|
||||
Array.Empty<string>(),
|
||||
Array.Empty<AdvisoryReference>(),
|
||||
Array.Empty<AffectedPackage>(),
|
||||
Array.Empty<CvssMetric>(),
|
||||
Array.Empty<AdvisoryProvenance>())),
|
||||
ImmutableArray.Create(new byte[] { 0x01, 0x02 }),
|
||||
DateTimeOffset.Parse("2025-10-04T00:00:00Z"),
|
||||
DateTimeOffset.Parse("2025-10-04T01:00:00Z"),
|
||||
ImmutableArray<Guid>.Empty),
|
||||
new AdvisoryStatementEntry(
|
||||
Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
||||
"cve-2025-0001",
|
||||
"adv-1",
|
||||
CanonicalJsonSerializer.Serialize(new Advisory(
|
||||
"adv-1",
|
||||
"A title",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
DateTimeOffset.Parse("2025-10-01T00:00:00Z"),
|
||||
null,
|
||||
false,
|
||||
Array.Empty<string>(),
|
||||
Array.Empty<AdvisoryReference>(),
|
||||
Array.Empty<AffectedPackage>(),
|
||||
Array.Empty<CvssMetric>(),
|
||||
Array.Empty<AdvisoryProvenance>())),
|
||||
ImmutableArray.Create(new byte[] { 0x03, 0x04 }),
|
||||
DateTimeOffset.Parse("2025-10-03T00:00:00Z"),
|
||||
DateTimeOffset.Parse("2025-10-04T02:00:00Z"),
|
||||
ImmutableArray<Guid>.Empty),
|
||||
});
|
||||
|
||||
repository.StoredConflicts.Add(new AdvisoryConflictEntry(
|
||||
Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc"),
|
||||
"cve-2025-0001",
|
||||
CanonicalJson: "{\"reason\":\"conflict\"}",
|
||||
ConflictHash: ImmutableArray.Create(new byte[] { 0x10 }),
|
||||
AsOf: DateTimeOffset.Parse("2025-10-04T00:00:00Z"),
|
||||
RecordedAt: DateTimeOffset.Parse("2025-10-04T03:00:00Z"),
|
||||
StatementIds: ImmutableArray<Guid>.Empty));
|
||||
|
||||
var replay = await log.ReplayAsync("CVE-2025-0001", asOf: null, CancellationToken.None);
|
||||
|
||||
Assert.Equal("cve-2025-0001", replay.VulnerabilityKey);
|
||||
Assert.Collection(
|
||||
replay.Statements,
|
||||
first => Assert.Equal("adv-2", first.AdvisoryKey),
|
||||
second => Assert.Equal("adv-1", second.AdvisoryKey));
|
||||
Assert.Single(replay.Conflicts);
|
||||
Assert.Equal("{\"reason\":\"conflict\"}", replay.Conflicts[0].CanonicalJson);
|
||||
}
|
||||
|
||||
private sealed class FakeRepository : IAdvisoryEventRepository
|
||||
{
|
||||
public List<AdvisoryStatementEntry> InsertedStatements { get; } = new();
|
||||
|
||||
public List<AdvisoryConflictEntry> InsertedConflicts { get; } = new();
|
||||
|
||||
public List<AdvisoryStatementEntry> StoredStatements { get; } = new();
|
||||
|
||||
public List<AdvisoryConflictEntry> StoredConflicts { get; } = new();
|
||||
|
||||
public ValueTask InsertStatementsAsync(IReadOnlyCollection<AdvisoryStatementEntry> statements, CancellationToken cancellationToken)
|
||||
{
|
||||
InsertedStatements.AddRange(statements);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask InsertConflictsAsync(IReadOnlyCollection<AdvisoryConflictEntry> conflicts, CancellationToken cancellationToken)
|
||||
{
|
||||
InsertedConflicts.AddRange(conflicts);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask<IReadOnlyList<AdvisoryStatementEntry>> GetStatementsAsync(string vulnerabilityKey, DateTimeOffset? asOf, CancellationToken cancellationToken)
|
||||
=> ValueTask.FromResult<IReadOnlyList<AdvisoryStatementEntry>>(StoredStatements.Where(entry =>
|
||||
string.Equals(entry.VulnerabilityKey, vulnerabilityKey, StringComparison.Ordinal) &&
|
||||
(!asOf.HasValue || entry.AsOf <= asOf.Value)).ToList());
|
||||
|
||||
public ValueTask<IReadOnlyList<AdvisoryConflictEntry>> GetConflictsAsync(string vulnerabilityKey, DateTimeOffset? asOf, CancellationToken cancellationToken)
|
||||
=> ValueTask.FromResult<IReadOnlyList<AdvisoryConflictEntry>>(StoredConflicts.Where(entry =>
|
||||
string.Equals(entry.VulnerabilityKey, vulnerabilityKey, StringComparison.Ordinal) &&
|
||||
(!asOf.HasValue || entry.AsOf <= asOf.Value)).ToList());
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _now;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset now)
|
||||
{
|
||||
_now = now.ToUniversalTime();
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user