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

@@ -80,6 +80,207 @@ public sealed class AttestorSubmissionServiceTests
Assert.Equal(first.Uuid, stored!.RekorUuid);
}
[Fact]
public async Task Validator_ThrowsWhenModeNotAllowed()
{
var canonicalizer = new DefaultDsseCanonicalizer();
var validator = new AttestorSubmissionValidator(canonicalizer, new[] { "kms" });
var request = CreateValidRequest(canonicalizer);
request.Bundle.Mode = "keyless";
await Assert.ThrowsAsync<AttestorValidationException>(() => validator.ValidateAsync(request));
}
[Fact]
public async Task SubmitAsync_Throws_WhenMirrorDisabledButRequested()
{
var options = Options.Create(new AttestorOptions
{
Redis = new AttestorOptions.RedisOptions { Url = string.Empty },
Rekor = new AttestorOptions.RekorOptions
{
Primary = new AttestorOptions.RekorBackendOptions
{
Url = "https://rekor.primary.test",
ProofTimeoutMs = 1000,
PollIntervalMs = 50,
MaxAttempts = 2
}
}
});
var canonicalizer = new DefaultDsseCanonicalizer();
var validator = new AttestorSubmissionValidator(canonicalizer);
var repository = new InMemoryAttestorEntryRepository();
var dedupeStore = new InMemoryAttestorDedupeStore();
var rekorClient = new StubRekorClient(new NullLogger<StubRekorClient>());
var archiveStore = new NullAttestorArchiveStore(new NullLogger<NullAttestorArchiveStore>());
var auditSink = new InMemoryAttestorAuditSink();
var logger = new NullLogger<AttestorSubmissionService>();
using var metrics = new AttestorMetrics();
var service = new AttestorSubmissionService(
validator,
repository,
dedupeStore,
rekorClient,
archiveStore,
auditSink,
options,
logger,
TimeProvider.System,
metrics);
var request = CreateValidRequest(canonicalizer);
request.Meta.LogPreference = "mirror";
var context = new SubmissionContext
{
CallerSubject = "urn:stellaops:signer",
CallerAudience = "attestor",
CallerClientId = "signer-service",
CallerTenant = "default"
};
var ex = await Assert.ThrowsAsync<AttestorValidationException>(() => service.SubmitAsync(request, context));
Assert.Equal("mirror_disabled", ex.Code);
}
[Fact]
public async Task SubmitAsync_ReturnsMirrorMetadata_WhenPreferenceBoth()
{
var options = Options.Create(new AttestorOptions
{
Redis = new AttestorOptions.RedisOptions { Url = string.Empty },
Rekor = new AttestorOptions.RekorOptions
{
Primary = new AttestorOptions.RekorBackendOptions
{
Url = "https://rekor.primary.test",
ProofTimeoutMs = 1000,
PollIntervalMs = 50,
MaxAttempts = 2
},
Mirror = new AttestorOptions.RekorMirrorOptions
{
Enabled = true,
Url = "https://rekor.mirror.test",
ProofTimeoutMs = 1000,
PollIntervalMs = 50,
MaxAttempts = 2
}
}
});
var canonicalizer = new DefaultDsseCanonicalizer();
var validator = new AttestorSubmissionValidator(canonicalizer);
var repository = new InMemoryAttestorEntryRepository();
var dedupeStore = new InMemoryAttestorDedupeStore();
var rekorClient = new StubRekorClient(new NullLogger<StubRekorClient>());
var archiveStore = new NullAttestorArchiveStore(new NullLogger<NullAttestorArchiveStore>());
var auditSink = new InMemoryAttestorAuditSink();
var logger = new NullLogger<AttestorSubmissionService>();
using var metrics = new AttestorMetrics();
var service = new AttestorSubmissionService(
validator,
repository,
dedupeStore,
rekorClient,
archiveStore,
auditSink,
options,
logger,
TimeProvider.System,
metrics);
var request = CreateValidRequest(canonicalizer);
request.Meta.LogPreference = "both";
var context = new SubmissionContext
{
CallerSubject = "urn:stellaops:signer",
CallerAudience = "attestor",
CallerClientId = "signer-service",
CallerTenant = "default"
};
var result = await service.SubmitAsync(request, context);
Assert.NotNull(result.Mirror);
Assert.False(string.IsNullOrEmpty(result.Mirror!.Uuid));
Assert.Equal("included", result.Mirror.Status);
}
[Fact]
public async Task SubmitAsync_UsesMirrorAsCanonical_WhenPreferenceMirror()
{
var options = Options.Create(new AttestorOptions
{
Redis = new AttestorOptions.RedisOptions { Url = string.Empty },
Rekor = new AttestorOptions.RekorOptions
{
Primary = new AttestorOptions.RekorBackendOptions
{
Url = "https://rekor.primary.test",
ProofTimeoutMs = 1000,
PollIntervalMs = 50,
MaxAttempts = 2
},
Mirror = new AttestorOptions.RekorMirrorOptions
{
Enabled = true,
Url = "https://rekor.mirror.test",
ProofTimeoutMs = 1000,
PollIntervalMs = 50,
MaxAttempts = 2
}
}
});
var canonicalizer = new DefaultDsseCanonicalizer();
var validator = new AttestorSubmissionValidator(canonicalizer);
var repository = new InMemoryAttestorEntryRepository();
var dedupeStore = new InMemoryAttestorDedupeStore();
var rekorClient = new StubRekorClient(new NullLogger<StubRekorClient>());
var archiveStore = new NullAttestorArchiveStore(new NullLogger<NullAttestorArchiveStore>());
var auditSink = new InMemoryAttestorAuditSink();
var logger = new NullLogger<AttestorSubmissionService>();
using var metrics = new AttestorMetrics();
var service = new AttestorSubmissionService(
validator,
repository,
dedupeStore,
rekorClient,
archiveStore,
auditSink,
options,
logger,
TimeProvider.System,
metrics);
var request = CreateValidRequest(canonicalizer);
request.Meta.LogPreference = "mirror";
var context = new SubmissionContext
{
CallerSubject = "urn:stellaops:signer",
CallerAudience = "attestor",
CallerClientId = "signer-service",
CallerTenant = "default"
};
var result = await service.SubmitAsync(request, context);
Assert.NotNull(result.Uuid);
var stored = await repository.GetByBundleShaAsync(request.Meta.BundleSha256);
Assert.NotNull(stored);
Assert.Equal("mirror", stored!.Log.Backend);
Assert.Null(result.Mirror);
}
private static AttestorSubmissionRequest CreateValidRequest(DefaultDsseCanonicalizer canonicalizer)
{
var request = new AttestorSubmissionRequest