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