// Copyright (c) Microsoft. All rights reserved. using System; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Moq; namespace Microsoft.Agents.AI.Hosting.UnitTests; /// /// Unit tests for . /// public class ClaimsIdentitySessionIsolationKeyProviderTests { private const string TestUserId = "test-user-id"; private const string CustomClaimType = "custom-claim-type"; private const string CustomClaimValue = "custom-claim-value"; private readonly Mock _httpContextAccessorMock; /// /// Initializes a new instance of the class. /// public ClaimsIdentitySessionIsolationKeyProviderTests() { this._httpContextAccessorMock = new Mock(); } #region Constructor Tests /// /// Verify that constructor uses default options when options is null. /// [Fact] public void UsesDefaultOptionsWhenNull() { // Act & Assert - should not throw var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object, options: null); Assert.NotNull(provider); } /// /// Verify that constructor accepts null IHttpContextAccessor. /// [Fact] public void Constructor_WithNullHttpContextAccessor_DoesNotThrow() { // Act & Assert - should not throw var provider = new ClaimsIdentitySessionIsolationKeyProvider(httpContextAccessor: null); Assert.NotNull(provider); } /// /// Verify that constructor throws ArgumentException when claimType is null. /// [Fact] public void RequiresClaimType_NotNull() { // Act & Assert Assert.Throws("options.ClaimType", () => new ClaimsIdentitySessionIsolationKeyProvider( this._httpContextAccessorMock.Object, new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = null! })); } /// /// Verify that constructor throws ArgumentException when claimType is empty. /// [Fact] public void RequiresClaimType_NotEmpty() { // Act & Assert Assert.Throws("options.ClaimType", () => new ClaimsIdentitySessionIsolationKeyProvider( this._httpContextAccessorMock.Object, new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = string.Empty })); } /// /// Verify that constructor throws ArgumentException when claimType is whitespace. /// [Fact] public void RequiresClaimType_NotWhitespace() { // Act & Assert Assert.Throws("options.ClaimType", () => new ClaimsIdentitySessionIsolationKeyProvider( this._httpContextAccessorMock.Object, new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = " " })); } #endregion #region GetSessionIsolationKeyAsync Tests /// /// Verify that GetSessionIsolationKeyAsync extracts the claim value from the default claim type. /// [Fact] public async Task GetSessionIsolationKeyAsyncExtractsDefaultClaimTypeAsync() { // Arrange this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, TestUserId); var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); // Act string? result = await provider.GetSessionIsolationKeyAsync(); // Assert Assert.Equal(TestUserId, result); } /// /// Verify that GetSessionIsolationKeyAsync uses custom claim type when specified. /// [Fact] public async Task GetSessionIsolationKeyAsyncUsesCustomClaimTypeAsync() { // Arrange this.SetupHttpContextWithClaim(CustomClaimType, CustomClaimValue); var provider = new ClaimsIdentitySessionIsolationKeyProvider( this._httpContextAccessorMock.Object, new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = CustomClaimType }); // Act string? result = await provider.GetSessionIsolationKeyAsync(); // Assert Assert.Equal(CustomClaimValue, result); } /// /// Verify that GetSessionIsolationKeyAsync returns null when the specified claim is missing. /// [Fact] public async Task GetSessionIsolationKeyAsyncReturnsNullWhenClaimMissingAsync() { // Arrange this.SetupHttpContextWithClaim("other-claim", "value"); var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); // Act string? result = await provider.GetSessionIsolationKeyAsync(); // Assert Assert.Null(result); } /// /// Verify behavior when HttpContextAccessor returns null HttpContext. /// [Fact] public async Task GetSessionIsolationKeyAsyncReturnsNullWhenHttpContextNullAsync() { // Arrange this._httpContextAccessorMock.Setup(x => x.HttpContext).Returns((HttpContext?)null); var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); // Act string? result = await provider.GetSessionIsolationKeyAsync(); // Assert Assert.Null(result); } /// /// Verify behavior when HttpContextAccessor itself is null. /// [Fact] public async Task GetSessionIsolationKeyAsyncReturnsNullWhenHttpContextAccessorNullAsync() { // Arrange var provider = new ClaimsIdentitySessionIsolationKeyProvider(httpContextAccessor: null); // Act string? result = await provider.GetSessionIsolationKeyAsync(); // Assert Assert.Null(result); } /// /// Verify that GetSessionIsolationKeyAsync returns the first matching claim when multiple exist. /// [Fact] public async Task GetSessionIsolationKeyAsyncReturnsFirstMatchingClaimAsync() { // Arrange const string FirstValue = "first-value"; const string SecondValue = "second-value"; var claims = new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, FirstValue), new Claim(ClaimsIdentity.DefaultNameClaimType, SecondValue), }; var identity = new ClaimsIdentity(claims); var principal = new ClaimsPrincipal(identity); var httpContext = new DefaultHttpContext { User = principal }; this._httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext); var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); // Act string? result = await provider.GetSessionIsolationKeyAsync(); // Assert Assert.Equal(FirstValue, result); } /// /// Verify that GetSessionIsolationKeyAsync handles empty claim values. /// [Fact] public async Task GetSessionIsolationKeyAsyncHandlesEmptyClaimValueAsync() { // Arrange this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, string.Empty); var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); // Act string? result = await provider.GetSessionIsolationKeyAsync(); // Assert Assert.Equal(string.Empty, result); } #endregion #region Helper Methods private void SetupHttpContextWithClaim(string claimType, string claimValue) { var claims = new[] { new Claim(claimType, claimValue) }; var identity = new ClaimsIdentity(claims); var principal = new ClaimsPrincipal(identity); var httpContext = new DefaultHttpContext { User = principal }; this._httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext); } #endregion }