File: DashboardOptionsTests.cs
Web Access
Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Security.Claims;
using System.Text.Json;
using Aspire.Dashboard.Configuration;
using Aspire.Hosting;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
using OpenIdConnectOptions = Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions;
 
namespace Aspire.Dashboard.Tests;
 
public sealed class DashboardOptionsTests
{
    private static DashboardOptions GetValidOptions()
    {
        // The minimal set of options required to pass validation.
        return new()
        {
            Frontend =
            {
                AuthMode = FrontendAuthMode.Unsecured,
                EndpointUrls = "http://localhost:5000"
            },
            Otlp =
            {
                AuthMode = OtlpAuthMode.Unsecured,
                GrpcEndpointUrl = "http://localhost:4317"
            },
        };
    }
 
    [Fact]
    public void ValidOptions_AreValid()
    {
        var result = new ValidateDashboardOptions().Validate(null, GetValidOptions());
 
        Assert.Null(result.FailureMessage);
        Assert.True(result.Succeeded);
    }
 
    #region Frontend options
 
    [Fact]
    public void FrontendOptions_EmptyEndpointUrl()
    {
        var options = GetValidOptions();
        options.Frontend.EndpointUrls = "";
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal("One or more frontend endpoint URLs are not configured. Specify an ASPNETCORE_URLS value.", result.FailureMessage);
    }
 
    [Fact]
    public void FrontendOptions_InvalidUrl()
    {
        var options = GetValidOptions();
        options.Frontend.EndpointUrls = "invalid";
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal("Failed to parse frontend endpoint URLs 'invalid'.", result.FailureMessage);
    }
 
    [Fact]
    public void FrontendOptions_ValidAndInvalidUrl()
    {
        var options = GetValidOptions();
        options.Frontend.EndpointUrls = "http://localhost:5000;invalid";
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal("Failed to parse frontend endpoint URLs 'http://localhost:5000;invalid'.", result.FailureMessage);
    }
 
    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    public void FrontendOptions_MaxConsoleLogCount(int limit)
    {
        var options = GetValidOptions();
        options.Frontend.MaxConsoleLogCount = limit;
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal($"{DashboardConfigNames.DashboardFrontendMaxConsoleLogCountName.ConfigKey} must be greater than zero.", result.FailureMessage);
    }
 
    #endregion
 
    #region Resource service client options
 
    [Fact]
    public void ResourceServiceClientOptions_InvalidUrl()
    {
        var options = GetValidOptions();
        options.ResourceServiceClient.Url = "invalid";
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal("Failed to parse resource service client endpoint URL 'invalid'.", result.FailureMessage);
    }
 
    [Fact]
    public void ResourceServiceClientOptions_ApiKeyMode_Empty()
    {
        var options = GetValidOptions();
        options.ResourceServiceClient.Url = "http://localhost";
        options.ResourceServiceClient.AuthMode = ResourceClientAuthMode.ApiKey;
        options.ResourceServiceClient.ApiKey = "";
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal($"{DashboardConfigNames.ResourceServiceClientAuthModeName.ConfigKey} is \"{nameof(ResourceClientAuthMode.ApiKey)}\", but no {DashboardConfigNames.ResourceServiceClientApiKeyName.ConfigKey} is configured.", result.FailureMessage);
    }
 
    [Fact]
    public void ResourceServiceClientOptions_CertificateMode_FileSource_FilePathEmpty()
    {
        var options = GetValidOptions();
        options.ResourceServiceClient.Url = "http://localhost";
        options.ResourceServiceClient.AuthMode = ResourceClientAuthMode.Certificate;
        options.ResourceServiceClient.ClientCertificate.Source = DashboardClientCertificateSource.File;
        options.ResourceServiceClient.ClientCertificate.FilePath = "";
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal($"{DashboardConfigNames.ResourceServiceClientCertificateSourceName.ConfigKey} is \"File\", but no {DashboardConfigNames.ResourceServiceClientCertificateFilePathName.ConfigKey} is configured.", result.FailureMessage);
    }
 
    [Fact]
    public void ResourceServiceClientOptions_CertificateMode_KeyStoreSource_SubjectEmpty()
    {
        var options = GetValidOptions();
        options.ResourceServiceClient.Url = "http://localhost";
        options.ResourceServiceClient.AuthMode = ResourceClientAuthMode.Certificate;
        options.ResourceServiceClient.ClientCertificate.Source = DashboardClientCertificateSource.KeyStore;
        options.ResourceServiceClient.ClientCertificate.Subject = "";
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal($"{DashboardConfigNames.ResourceServiceClientCertificateSourceName.ConfigKey} is \"KeyStore\", but no {DashboardConfigNames.ResourceServiceClientCertificateSubjectName.ConfigKey} is configured.", result.FailureMessage);
    }
 
    [Fact]
    public void ResourceServiceClientOptions_CertificateMode_NullSource()
    {
        var options = GetValidOptions();
        options.ResourceServiceClient.Url = "http://localhost";
        options.ResourceServiceClient.AuthMode = ResourceClientAuthMode.Certificate;
        options.ResourceServiceClient.ClientCertificate.Source = null;
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal($"The resource service client is configured to use certificates, but no certificate source is specified. Specify {DashboardConfigNames.ResourceServiceClientCertificateSourceName.ConfigKey}. Possible values: {string.Join(", ", typeof(DashboardClientCertificateSource).GetEnumNames())}", result.FailureMessage);
    }
 
    [Fact]
    public void ResourceServiceClientOptions_CertificateMode_InvalidSource()
    {
        var options = GetValidOptions();
        options.ResourceServiceClient.Url = "http://localhost";
        options.ResourceServiceClient.AuthMode = ResourceClientAuthMode.Certificate;
        options.ResourceServiceClient.ClientCertificate.Source = (DashboardClientCertificateSource)int.MaxValue;
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal($"Unexpected resource service client certificate source: {options.ResourceServiceClient.ClientCertificate.Source}", result.FailureMessage);
    }
 
    [Fact]
    public void ResourceServiceClientOptions_NullMode()
    {
        var options = GetValidOptions();
        options.ResourceServiceClient.Url = "http://localhost";
        options.ResourceServiceClient.AuthMode = null;
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal($"Resource service client authentication is not configured. Specify {DashboardConfigNames.ResourceServiceClientAuthModeName.ConfigKey}. Possible values: {string.Join(", ", typeof(ResourceClientAuthMode).GetEnumNames())}", result.FailureMessage);
    }
 
    [Fact]
    public void ResourceServiceClientOptions_InvalidMode()
    {
        var options = GetValidOptions();
        options.ResourceServiceClient.Url = "http://localhost";
        options.ResourceServiceClient.AuthMode = (ResourceClientAuthMode)int.MaxValue;
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal($"Unexpected resource service client authentication mode: {int.MaxValue}", result.FailureMessage);
    }
 
    #endregion
 
    #region OTLP options
 
    [Fact]
    public void OtlpOptions_NeitherEndpointSet()
    {
        var options = GetValidOptions();
        options.Otlp.GrpcEndpointUrl = null;
        options.Otlp.HttpEndpointUrl = null;
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal(
            $"Neither OTLP/gRPC or OTLP/HTTP endpoint URLs are configured. Specify either a {DashboardConfigNames.DashboardOtlpGrpcUrlName.EnvVarName} or {DashboardConfigNames.DashboardOtlpHttpUrlName.EnvVarName} value.",
            result.FailureMessage);
    }
 
    [Fact]
    public void OtlpOptions_gRPC_InvalidUrl()
    {
        var options = GetValidOptions();
        options.Otlp.GrpcEndpointUrl = "invalid";
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal("Failed to parse OTLP gRPC endpoint URL 'invalid'.", result.FailureMessage);
    }
 
    [Fact]
    public void OtlpOptions_HTTP_InvalidUrl()
    {
        var options = GetValidOptions();
        options.Otlp.HttpEndpointUrl = "invalid";
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal("Failed to parse OTLP HTTP endpoint URL 'invalid'.", result.FailureMessage);
    }
 
    [Fact]
    public async Task OtlpOptions_SuppressUnsecuredMessage_LegacyName()
    {
        await using var app = new DashboardWebApplication(builder => builder.Configuration.AddInMemoryCollection(
        [
            new("ASPNETCORE_URLS", "http://localhost:8000/"),
            new("ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL", "http://localhost:4319/"),
            new(DashboardConfigNames.Legacy.DashboardOtlpSuppressUnsecuredTelemetryMessage.ConfigKey, "true"),
        ]));
        var options = app.Services.GetService<IOptionsMonitor<DashboardOptions>>()!;
 
        Assert.True(options.CurrentValue.Otlp.SuppressUnsecuredMessage);
    }
 
    #endregion
 
    #region OpenIDConnect options
 
    [Fact]
    public void OpenIdConnectOptions_NoNameClaimType()
    {
        var options = GetValidOptions();
        options.Frontend.AuthMode = FrontendAuthMode.OpenIdConnect;
        options.Frontend.OpenIdConnect.NameClaimType = "";
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal("OpenID Connect claim type for name not configured. Specify a Dashboard:Frontend:OpenIdConnect:NameClaimType value.", result.FailureMessage);
    }
 
    [Fact]
    public void OpenIdConnectOptions_NoUserNameClaimType()
    {
        var options = GetValidOptions();
        options.Frontend.AuthMode = FrontendAuthMode.OpenIdConnect;
        options.Frontend.OpenIdConnect.UsernameClaimType = "";
 
        var result = new ValidateDashboardOptions().Validate(null, options);
 
        Assert.False(result.Succeeded);
        Assert.Equal("OpenID Connect claim type for username not configured. Specify a Dashboard:Frontend:OpenIdConnect:UsernameClaimType value.", result.FailureMessage);
    }
 
    [Fact]
    public async Task OpenIdConnectOptions_ClaimActions_MapJsonKeyTestAsync()
    {
        await using var app = new DashboardWebApplication(builder => builder.Configuration.AddInMemoryCollection(
        [
            new("ASPNETCORE_URLS", "http://localhost:8000/"),
            new("ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL", "http://localhost:4319/"),
            new("Authentication:Schemes:OpenIdConnect:Authority", "https://id.aspire.dev/"),
            new("Authentication:Schemes:OpenIdConnect:ClientId", "aspire-dashboard"),
            new("Dashboard:Frontend:AuthMode", "OpenIdConnect"),
            new("Dashboard:Frontend:OpenIdConnect:ClaimActions:0:ClaimType", "role"),
            new("Dashboard:Frontend:OpenIdConnect:ClaimActions:0:JsonKey", "role"),
            new("Dashboard:Frontend:OpenIdConnect:RequiredClaimType", "role")
        ]));
        var openIdConnectAuthOptions = app.Services.GetService<IOptionsMonitor<OpenIdConnectOptions>>()?.Get(OpenIdConnectDefaults.AuthenticationScheme);
        Assert.NotNull(openIdConnectAuthOptions);
        Assert.NotEmpty(openIdConnectAuthOptions.ClaimActions);
        var claimAction = openIdConnectAuthOptions.ClaimActions.FirstOrDefault(x => x.ClaimType == "role");
        Assert.NotNull(claimAction);
        Assert.Equal("role", claimAction.ClaimType);
        var jsonElement = JsonDocument.Parse("""
                           {
                             "role": ["admin", "test"]
                           }
                           """).RootElement.Clone();
        var claimIdentity = new ClaimsIdentity();
        claimAction.Run(jsonElement, claimIdentity, "test");
        Assert.Equal(2, claimIdentity.Claims.Count());
        Assert.True(claimIdentity.HasClaim("role", "admin"));
        Assert.True(claimIdentity.HasClaim("role", "test"));
    }
 
    [Fact]
    public void GetOidcClaimActionConfigure_MapJsonKeyTest()
    {
        var claimAction = new ClaimAction
        {
            ClaimType = "role",
            JsonKey = "role"
        };
        var oidcOption = new OpenIdConnectOptions();
        oidcOption.ClaimActions.Clear();
        var configure = DashboardWebApplication.GetOidcClaimActionConfigure(claimAction);
        configure(oidcOption);
        Assert.Single(oidcOption.ClaimActions);
        Assert.Contains(oidcOption.ClaimActions, x => x.ClaimType == claimAction.ClaimType && x.ValueType == ClaimValueTypes.String);
        var action = oidcOption.ClaimActions.FirstOrDefault(x => x.ClaimType == claimAction.ClaimType);
        Assert.NotNull(action);
        var jsonElement = JsonDocument.Parse("""
                                             {
                                               "role": ["admin", "test"]
                                             }
                                             """).RootElement.Clone();
        var claimIdentity = new ClaimsIdentity();
        action.Run(jsonElement, claimIdentity, "test");
        Assert.Equal(2, claimIdentity.Claims.Count());
        Assert.True(claimIdentity.HasClaim("role", "admin"));
        Assert.True(claimIdentity.HasClaim("role", "test"));
    }
 
    [Fact]
    public void GetOidcClaimActionConfigure_MapUniqueJsonKeyTest()
    {
        var claimAction = new ClaimAction
        {
            ClaimType = "name",
            JsonKey = "name",
            IsUnique = true
        };
        var oidcOption = new OpenIdConnectOptions();
        oidcOption.ClaimActions.Clear();
        var configure = DashboardWebApplication.GetOidcClaimActionConfigure(claimAction);
        configure(oidcOption);
        Assert.Single(oidcOption.ClaimActions);
        Assert.Contains(oidcOption.ClaimActions, x => x.ClaimType == claimAction.ClaimType && x.ValueType == ClaimValueTypes.String);
        var action = oidcOption.ClaimActions.FirstOrDefault(x => x.ClaimType == claimAction.ClaimType);
        Assert.NotNull(action);
        var jsonElement = JsonDocument.Parse("""
                                             {
                                               "name": "test"
                                             }
                                             """).RootElement.Clone();
        var claimIdentity = new ClaimsIdentity(
        [
            new Claim("name", "test")
        ]);
        action.Run(jsonElement, claimIdentity, "test");
        Assert.Single(claimIdentity.Claims);
        Assert.True(claimIdentity.HasClaim("name", "test"));
 
        var emptyClaimIdentity = new ClaimsIdentity();
        action.Run(jsonElement, emptyClaimIdentity, "test");
        Assert.Single(emptyClaimIdentity.Claims);
        Assert.True(emptyClaimIdentity.HasClaim("name", "test"));
    }
 
    [Fact]
    public void GetOidcClaimActionConfigure_MapJsonSubKeyTest()
    {
        var claimAction = new ClaimAction
        {
            ClaimType = "name",
            JsonKey = "profile",
            SubKey = "name"
        };
        var oidcOption = new OpenIdConnectOptions();
        oidcOption.ClaimActions.Clear();
        var configure = DashboardWebApplication.GetOidcClaimActionConfigure(claimAction);
        configure(oidcOption);
        Assert.Single(oidcOption.ClaimActions);
        Assert.Contains(oidcOption.ClaimActions, x => x.ClaimType == claimAction.ClaimType && x.ValueType == ClaimValueTypes.String);
        var action = oidcOption.ClaimActions.FirstOrDefault(x => x.ClaimType == claimAction.ClaimType);
        Assert.NotNull(action);
        var jsonElement = JsonDocument.Parse("""
                                             {
                                               "profile": {
                                                 "name": "test"
                                               }
                                             }
                                             """).RootElement.Clone();
        var claimIdentity = new ClaimsIdentity(
        [
            new Claim("name", "test")
        ]);
        action.Run(jsonElement, claimIdentity, "test");
        Assert.Equal(2, claimIdentity.Claims.Count());
        Assert.True(claimIdentity.HasClaim("name", "test"));
 
        var emptyClaimIdentity = new ClaimsIdentity();
        action.Run(jsonElement, emptyClaimIdentity, "test");
        Assert.Single(emptyClaimIdentity.Claims);
        Assert.True(emptyClaimIdentity.HasClaim("name", "test"));
    }
 
    [Fact]
    public void GetOidcClaimActionConfigure_MapJsonKey_ValueTypeTest()
    {
        var claimAction = new ClaimAction
        {
            ClaimType = "sub",
            JsonKey = "userId",
            ValueType = ClaimValueTypes.Integer,
            IsUnique = true
        };
        var oidcOption = new OpenIdConnectOptions();
        oidcOption.ClaimActions.Clear();
        var configure = DashboardWebApplication.GetOidcClaimActionConfigure(claimAction);
        configure(oidcOption);
        Assert.Single(oidcOption.ClaimActions);
        Assert.Contains(oidcOption.ClaimActions, x => x.ClaimType == claimAction.ClaimType && x.ValueType == claimAction.ValueType);
        var action = oidcOption.ClaimActions.FirstOrDefault(x => x.ClaimType == claimAction.ClaimType);
        Assert.NotNull(action);
        var jsonElement = JsonDocument.Parse("""
                                             {
                                               "userId": "1"
                                             }
                                             """).RootElement.Clone();
        var claimIdentity = new ClaimsIdentity();
        action.Run(jsonElement, claimIdentity, "test");
        Assert.NotEmpty(claimIdentity.Claims);
        Assert.True(claimIdentity.HasClaim("sub", "1"));
    }
 
    #endregion
}