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:
master
2025-10-19 23:29:34 +03:00
parent a811f7ac47
commit a07f46231b
239 changed files with 17245 additions and 3155 deletions

View File

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