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:
@@ -7,6 +7,7 @@ public static class AuthorityClientMetadataKeys
|
||||
{
|
||||
public const string AllowedGrantTypes = "allowedGrantTypes";
|
||||
public const string AllowedScopes = "allowedScopes";
|
||||
public const string Audiences = "audiences";
|
||||
public const string RedirectUris = "redirectUris";
|
||||
public const string PostLogoutRedirectUris = "postLogoutRedirectUris";
|
||||
public const string SenderConstraint = "senderConstraint";
|
||||
|
||||
@@ -632,15 +632,13 @@ public sealed class AuthorityClaimsEnrichmentContext
|
||||
/// </summary>
|
||||
public sealed record AuthorityClientDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialises a new client descriptor.
|
||||
/// </summary>
|
||||
public AuthorityClientDescriptor(
|
||||
string clientId,
|
||||
string? displayName,
|
||||
bool confidential,
|
||||
IReadOnlyCollection<string>? allowedGrantTypes = null,
|
||||
IReadOnlyCollection<string>? allowedScopes = null,
|
||||
IReadOnlyCollection<string>? allowedAudiences = null,
|
||||
IReadOnlyCollection<Uri>? redirectUris = null,
|
||||
IReadOnlyCollection<Uri>? postLogoutRedirectUris = null,
|
||||
IReadOnlyDictionary<string, string?>? properties = null)
|
||||
@@ -648,8 +646,9 @@ public sealed record AuthorityClientDescriptor
|
||||
ClientId = ValidateRequired(clientId, nameof(clientId));
|
||||
DisplayName = displayName;
|
||||
Confidential = confidential;
|
||||
AllowedGrantTypes = allowedGrantTypes is null ? Array.Empty<string>() : allowedGrantTypes.ToArray();
|
||||
AllowedScopes = allowedScopes is null ? Array.Empty<string>() : allowedScopes.ToArray();
|
||||
AllowedGrantTypes = Normalize(allowedGrantTypes);
|
||||
AllowedScopes = Normalize(allowedScopes);
|
||||
AllowedAudiences = Normalize(allowedAudiences);
|
||||
RedirectUris = redirectUris is null ? Array.Empty<Uri>() : redirectUris.ToArray();
|
||||
PostLogoutRedirectUris = postLogoutRedirectUris is null ? Array.Empty<Uri>() : postLogoutRedirectUris.ToArray();
|
||||
Properties = properties is null
|
||||
@@ -657,60 +656,87 @@ public sealed record AuthorityClientDescriptor
|
||||
: new Dictionary<string, string?>(properties, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unique client identifier.
|
||||
/// </summary>
|
||||
public string ClientId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional display name.
|
||||
/// </summary>
|
||||
public string? DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the client is confidential (requires secret).
|
||||
/// </summary>
|
||||
public bool Confidential { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Permitted OAuth grant types.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> AllowedGrantTypes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Permitted scopes.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> AllowedScopes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registered redirect URIs.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> AllowedAudiences { get; }
|
||||
public IReadOnlyCollection<Uri> RedirectUris { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registered post-logout redirect URIs.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<Uri> PostLogoutRedirectUris { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional plugin-defined metadata.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string?> Properties { get; }
|
||||
|
||||
private static IReadOnlyCollection<string> Normalize(IReadOnlyCollection<string>? values)
|
||||
=> values is null || values.Count == 0
|
||||
? Array.Empty<string>()
|
||||
: values
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(value => value.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
private static string ValidateRequired(string value, string paramName)
|
||||
=> string.IsNullOrWhiteSpace(value)
|
||||
? throw new ArgumentException("Value cannot be null or whitespace.", paramName)
|
||||
: value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client registration payload used when provisioning clients through plugins.
|
||||
/// </summary>
|
||||
public sealed record AuthorityClientCertificateBindingRegistration
|
||||
{
|
||||
public AuthorityClientCertificateBindingRegistration(
|
||||
string thumbprint,
|
||||
string? serialNumber = null,
|
||||
string? subject = null,
|
||||
string? issuer = null,
|
||||
IReadOnlyCollection<string>? subjectAlternativeNames = null,
|
||||
DateTimeOffset? notBefore = null,
|
||||
DateTimeOffset? notAfter = null,
|
||||
string? label = null)
|
||||
{
|
||||
Thumbprint = NormalizeThumbprint(thumbprint);
|
||||
SerialNumber = Normalize(serialNumber);
|
||||
Subject = Normalize(subject);
|
||||
Issuer = Normalize(issuer);
|
||||
SubjectAlternativeNames = subjectAlternativeNames is null || subjectAlternativeNames.Count == 0
|
||||
? Array.Empty<string>()
|
||||
: subjectAlternativeNames
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(value => value.Trim())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
NotBefore = notBefore;
|
||||
NotAfter = notAfter;
|
||||
Label = Normalize(label);
|
||||
}
|
||||
|
||||
public string Thumbprint { get; }
|
||||
public string? SerialNumber { get; }
|
||||
public string? Subject { get; }
|
||||
public string? Issuer { get; }
|
||||
public IReadOnlyCollection<string> SubjectAlternativeNames { get; }
|
||||
public DateTimeOffset? NotBefore { get; }
|
||||
public DateTimeOffset? NotAfter { get; }
|
||||
public string? Label { get; }
|
||||
|
||||
private static string NormalizeThumbprint(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException("Thumbprint is required.", nameof(value));
|
||||
}
|
||||
|
||||
return value
|
||||
.Replace(":", string.Empty, StringComparison.Ordinal)
|
||||
.Replace(" ", string.Empty, StringComparison.Ordinal)
|
||||
.ToUpperInvariant();
|
||||
}
|
||||
|
||||
private static string? Normalize(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
|
||||
public sealed record AuthorityClientRegistration
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialises a new registration.
|
||||
/// </summary>
|
||||
public AuthorityClientRegistration(
|
||||
string clientId,
|
||||
bool confidential,
|
||||
@@ -718,9 +744,11 @@ public sealed record AuthorityClientRegistration
|
||||
string? clientSecret,
|
||||
IReadOnlyCollection<string>? allowedGrantTypes = null,
|
||||
IReadOnlyCollection<string>? allowedScopes = null,
|
||||
IReadOnlyCollection<string>? allowedAudiences = null,
|
||||
IReadOnlyCollection<Uri>? redirectUris = null,
|
||||
IReadOnlyCollection<Uri>? postLogoutRedirectUris = null,
|
||||
IReadOnlyDictionary<string, string?>? properties = null)
|
||||
IReadOnlyDictionary<string, string?>? properties = null,
|
||||
IReadOnlyCollection<AuthorityClientCertificateBindingRegistration>? certificateBindings = null)
|
||||
{
|
||||
ClientId = ValidateRequired(clientId, nameof(clientId));
|
||||
Confidential = confidential;
|
||||
@@ -728,65 +756,42 @@ public sealed record AuthorityClientRegistration
|
||||
ClientSecret = confidential
|
||||
? ValidateRequired(clientSecret ?? string.Empty, nameof(clientSecret))
|
||||
: clientSecret;
|
||||
AllowedGrantTypes = allowedGrantTypes is null ? Array.Empty<string>() : allowedGrantTypes.ToArray();
|
||||
AllowedScopes = allowedScopes is null ? Array.Empty<string>() : allowedScopes.ToArray();
|
||||
AllowedGrantTypes = Normalize(allowedGrantTypes);
|
||||
AllowedScopes = Normalize(allowedScopes);
|
||||
AllowedAudiences = Normalize(allowedAudiences);
|
||||
RedirectUris = redirectUris is null ? Array.Empty<Uri>() : redirectUris.ToArray();
|
||||
PostLogoutRedirectUris = postLogoutRedirectUris is null ? Array.Empty<Uri>() : postLogoutRedirectUris.ToArray();
|
||||
Properties = properties is null
|
||||
? new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
: new Dictionary<string, string?>(properties, StringComparer.OrdinalIgnoreCase);
|
||||
CertificateBindings = certificateBindings is null
|
||||
? Array.Empty<AuthorityClientCertificateBindingRegistration>()
|
||||
: certificateBindings.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unique client identifier.
|
||||
/// </summary>
|
||||
public string ClientId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the client is confidential (requires secret handling).
|
||||
/// </summary>
|
||||
public bool Confidential { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional display name.
|
||||
/// </summary>
|
||||
public string? DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional raw client secret (hashed by the plugin for storage).
|
||||
/// </summary>
|
||||
public string? ClientSecret { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Grant types to enable.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> AllowedGrantTypes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Scopes assigned to the client.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> AllowedScopes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Redirect URIs permitted for the client.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> AllowedAudiences { get; }
|
||||
public IReadOnlyCollection<Uri> RedirectUris { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Post-logout redirect URIs.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<Uri> PostLogoutRedirectUris { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata for the plugin.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string?> Properties { get; }
|
||||
public IReadOnlyCollection<AuthorityClientCertificateBindingRegistration> CertificateBindings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of the registration with the provided client secret.
|
||||
/// </summary>
|
||||
public AuthorityClientRegistration WithClientSecret(string? clientSecret)
|
||||
=> new(ClientId, Confidential, DisplayName, clientSecret, AllowedGrantTypes, AllowedScopes, RedirectUris, PostLogoutRedirectUris, Properties);
|
||||
=> new(ClientId, Confidential, DisplayName, clientSecret, AllowedGrantTypes, AllowedScopes, AllowedAudiences, RedirectUris, PostLogoutRedirectUris, Properties, CertificateBindings);
|
||||
|
||||
private static IReadOnlyCollection<string> Normalize(IReadOnlyCollection<string>? values)
|
||||
=> values is null || values.Count == 0
|
||||
? Array.Empty<string>()
|
||||
: values
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(value => value.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
private static string ValidateRequired(string value, string paramName)
|
||||
=> string.IsNullOrWhiteSpace(value)
|
||||
|
||||
Reference in New Issue
Block a user