Skip to content

Conversation

@mrunalhirve128
Copy link
Contributor

Add runtime unit tests

Copilot AI review requested due to automatic review settings December 5, 2025 06:23
@mrunalhirve128 mrunalhirve128 requested a review from a team as a code owner December 5, 2025 06:23
@mrunalhirve128 mrunalhirve128 marked this pull request as draft December 5, 2025 06:24
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive unit tests for runtime utility classes including Utility, AgenticAuthenticationService, and TenantContextHelper. The tests focus on configuration retrieval, environment detection, token handling, and tenant/worker context extraction from various HTTP sources.

Key Changes

  • Added unit tests for Utility class methods covering configuration scopes, environment detection, and JWT token parsing
  • Added unit tests for AgenticAuthenticationService covering authentication token retrieval
  • Updated TenantContextHelperTests with copyright headers and consolidated test cases using [Theory] attributes
  • Updated project namespace from Microsoft.Agents.A365.Runtime.Common.Tests to Microsoft.Agents.A365.Runtime.Tests
  • Added required JWT token handling dependencies (System.IdentityModel.Tokens.Jwt and Microsoft.IdentityModel.Tokens)

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/src/Tests/Runtime.Tests/UtilityTests.cs CRITICAL: File in wrong directory - Should be removed; tests belong in src/Tests/Runtime.Tests/
src/src/Tests/Runtime.Tests/AgenticAuthenticationServiceTests.cs CRITICAL: File in wrong directory - Should be removed; tests belong in src/Tests/Runtime.Tests/
src/Tests/Runtime.Tests/UtilityTests.cs Comprehensive unit tests for Utility class; has potential issues with empty string handling expectations
src/Tests/Runtime.Tests/AgenticAuthenticationServiceTests.cs Tests for utility methods but misnamed; tests Utility class, not AgenticAuthenticationService
src/Tests/Runtime.Tests/TenantContextHelperTests.cs Added copyright header, updated namespace, and consolidated tests using Theory attributes
src/Tests/Runtime.Tests/Microsoft.Agents.A365.Runtime.Tests.csproj Updated project assembly and namespace from Common.Tests to Runtime.Tests
src/Directory.Packages.props Added JWT token handling dependencies with appropriate versions (8.8.0)

Comment on lines +14 to +17
/// Unit tests for AgenticAuthenticationService class.
/// Tests the authentication token retrieval and utility methods.
/// </summary>
public class AgenticAuthenticationServiceTests
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name AgenticAuthenticationServiceTests suggests this class tests AgenticAuthenticationService, but the tests are actually testing Utility class methods (Utility.GetMcpPlatformAuthenticationScope and Utility.GetCurrentEnvironment).

Consider renaming this class to better reflect what it's testing, such as UtilityConfigurationTests or moving these tests into the existing UtilityTests class to avoid confusion.

Suggested change
/// Unit tests for AgenticAuthenticationService class.
/// Tests the authentication token retrieval and utility methods.
/// </summary>
public class AgenticAuthenticationServiceTests
/// Unit tests for configuration-related methods in the Utility class.
/// Tests Utility.GetMcpPlatformAuthenticationScope and Utility.GetCurrentEnvironment.
/// </summary>
public class UtilityConfigurationTests

Copilot uses AI. Check for mistakes.
Comment on lines 22 to 23
[InlineData("", "")] // Empty string is returned as-is (not replaced with default)
[InlineData(" ", " ")] // Whitespace is returned as-is (not replaced with default)
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test expects empty string "" and whitespace " " to be returned as-is, but this contradicts the default behavior pattern where null values return the default scope "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default" (as shown in line 21).

If the actual implementation treats empty/whitespace strings the same as null (returning the default), then these test cases will fail. If empty strings should indeed return as-is, this creates an inconsistent API where null returns a default but "" returns empty string.

Please verify the actual implementation behavior and ensure these test expectations are correct. Consider whether empty/whitespace strings should also trigger default value logic for consistency.

Suggested change
[InlineData("", "")] // Empty string is returned as-is (not replaced with default)
[InlineData(" ", " ")] // Whitespace is returned as-is (not replaced with default)
[InlineData("", "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default")] // Empty string is treated as null and replaced with default
[InlineData(" ", "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default")] // Whitespace is treated as null and replaced with default

Copilot uses AI. Check for mistakes.
Comment on lines +99 to +116
public void GetMcpPlatformAuthenticationScope_CalledMultipleTimes_AccessesConfigurationCorrectly(
string testScope,
int callCount)
{
// Arrange
var mockConfiguration = new Mock<IConfiguration>();
mockConfiguration.Setup(c => c["MCP_PLATFORM_AUTHENTICATION_SCOPE"])
.Returns(testScope);

// Act
for (int i = 0; i < callCount; i++)
{
Utility.GetMcpPlatformAuthenticationScope(mockConfiguration.Object);
}

// Assert
mockConfiguration.Verify(c => c["MCP_PLATFORM_AUTHENTICATION_SCOPE"], Times.Exactly(callCount));
}
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test at line 99 (GetMcpPlatformAuthenticationScope_CalledMultipleTimes_AccessesConfigurationCorrectly) doesn't verify the actual return value of the method being tested. It only verifies that the configuration was accessed the correct number of times.

While verifying interaction count can be useful, this test should also assert that the correct scope value is returned on each call to ensure the method behaves correctly, not just that it accesses configuration. Consider adding assertions like:

var result = Utility.GetMcpPlatformAuthenticationScope(mockConfiguration.Object);
Assert.Equal(testScope, result);

inside the loop or after it.

Copilot uses AI. Check for mistakes.
Comment on lines 30 to 31
[InlineData("", "")] // Empty string is returned as-is (not null)
[InlineData(" ", " ")] // Whitespace is returned as-is (not null)
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test expects empty strings "" and whitespace " " to be returned as-is (based on the comment), but this contradicts the default behavior pattern. Line 29 shows that when config is null (via "SKIP"), the default scope "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default" is returned.

If the implementation treats empty/whitespace strings the same as null (returning the default), these test expectations are incorrect. If empty strings truly return as-is while null returns a default, this creates an inconsistent API.

Please verify the actual implementation behavior and ensure the test expectations and comments accurately reflect it.

Suggested change
[InlineData("", "")] // Empty string is returned as-is (not null)
[InlineData(" ", " ")] // Whitespace is returned as-is (not null)
[InlineData("", "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default")] // Empty string is treated as null and returns default scope
[InlineData(" ", "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default")] // Whitespace is treated as null and returns default scope

Copilot uses AI. Check for mistakes.
Comment on lines 59 to 60
[InlineData("", "", "")] // Empty strings are returned as-is (not replaced with default)
[InlineData(" ", " ", " ")] // Whitespace is returned as-is (not replaced with default)
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test expects empty strings "" and whitespace " " to return as-is (based on the comment), but this contradicts the default behavior. Line 58 shows that when both config values are null (via "SKIP"), "Development" is returned as the default.

If the implementation treats empty/whitespace strings the same as null (returning "Development"), these test expectations are incorrect and the tests will fail. If empty strings truly return as-is while null returns a default, this creates an inconsistent API.

Please verify the actual implementation behavior and ensure the test expectations and comments accurately reflect it.

Suggested change
[InlineData("", "", "")] // Empty strings are returned as-is (not replaced with default)
[InlineData(" ", " ", " ")] // Whitespace is returned as-is (not replaced with default)
[InlineData("", "", "Development")] // Empty strings are treated as null and default to "Development"
[InlineData(" ", " ", "Development")] // Whitespace is treated as null and default to "Development"

Copilot uses AI. Check for mistakes.
Comment on lines 144 to 148
public void GetAppIdFromToken_WithInvalidTokenFormats_ThrowsException(string invalidToken)
{
// Act & Assert - Expects SecurityTokenMalformedException or ArgumentException
Assert.ThrowsAny<Exception>(() => Utility.GetAppIdFromToken(invalidToken));
}
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses Assert.ThrowsAny<Exception>() which is too broad and will catch any exception type. This makes the test less precise and could pass even if an unexpected exception type is thrown.

Based on the comment mentioning "SecurityTokenMalformedException or ArgumentException", consider using a more specific assertion:

var exception = Assert.ThrowsAny<Exception>(() => Utility.GetAppIdFromToken(invalidToken));
Assert.True(exception is SecurityTokenMalformedException || exception is ArgumentException,
    $"Expected SecurityTokenMalformedException or ArgumentException, but got {exception.GetType().Name}");

Or test each exception type separately if the behavior is deterministic for each invalid token format.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 382
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Agents.A365.Runtime.Utils;
using Microsoft.Agents.Builder;
using Microsoft.Extensions.Configuration;
using Moq;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using Xunit;

namespace Microsoft.Agents.A365.Runtime.Tests
{
/// <summary>
/// Unit tests for Utility class.
/// Tests configuration retrieval, environment detection, and token handling utilities.
/// </summary>
public class UtilityTests
{
#region GetMcpPlatformAuthenticationScope Tests

[Fact]
public void GetMcpPlatformAuthenticationScope_WithConfigurationValue_ReturnsConfigurationValue()
{
// Arrange
const string expectedScope = "custom-scope-from-config";
var mockConfiguration = new Mock<IConfiguration>();
mockConfiguration.Setup(c => c["MCP_PLATFORM_AUTHENTICATION_SCOPE"])
.Returns(expectedScope);

// Act
var result = Utility.GetMcpPlatformAuthenticationScope(mockConfiguration.Object);

// Assert
Assert.Equal(expectedScope, result);
}

[Fact]
public void GetMcpPlatformAuthenticationScope_WithNullConfigurationValue_ReturnsDefaultScope()
{
// Arrange
const string expectedDefaultScope = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default";
var mockConfiguration = new Mock<IConfiguration>();
mockConfiguration.Setup(c => c["MCP_PLATFORM_AUTHENTICATION_SCOPE"])
.Returns((string?)null);

// Act
var result = Utility.GetMcpPlatformAuthenticationScope(mockConfiguration.Object);

// Assert
Assert.Equal(expectedDefaultScope, result);
}

[Fact]
public void GetMcpPlatformAuthenticationScope_WithEmptyConfigurationValue_ReturnsDefaultScope()
{
// Arrange
const string expectedDefaultScope = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default";
var mockConfiguration = new Mock<IConfiguration>();
mockConfiguration.Setup(c => c["MCP_PLATFORM_AUTHENTICATION_SCOPE"])
.Returns(string.Empty);

// Act
var result = Utility.GetMcpPlatformAuthenticationScope(mockConfiguration.Object);

// Assert
Assert.Equal(expectedDefaultScope, result);
}

#endregion

#region GetCurrentEnvironment Tests

[Fact]
public void GetCurrentEnvironment_WithAspNetCoreEnvironment_ReturnsAspNetCoreValue()
{
// Arrange
const string expectedEnvironment = "Production";
var mockConfiguration = new Mock<IConfiguration>();
mockConfiguration.Setup(c => c["ASPNETCORE_ENVIRONMENT"])
.Returns(expectedEnvironment);
mockConfiguration.Setup(c => c["DOTNET_ENVIRONMENT"])
.Returns("SomeOtherValue");

// Act
var result = Utility.GetCurrentEnvironment(mockConfiguration.Object);

// Assert
Assert.Equal(expectedEnvironment, result);
}

[Fact]
public void GetCurrentEnvironment_WithDotNetEnvironmentOnly_ReturnsDotNetValue()
{
// Arrange
const string expectedEnvironment = "Staging";
var mockConfiguration = new Mock<IConfiguration>();
mockConfiguration.Setup(c => c["ASPNETCORE_ENVIRONMENT"])
.Returns((string?)null);
mockConfiguration.Setup(c => c["DOTNET_ENVIRONMENT"])
.Returns(expectedEnvironment);

// Act
var result = Utility.GetCurrentEnvironment(mockConfiguration.Object);

// Assert
Assert.Equal(expectedEnvironment, result);
}

[Fact]
public void GetCurrentEnvironment_WithNoEnvironmentVariables_ReturnsDefaultDevelopment()
{
// Arrange
const string expectedDefaultEnvironment = "Development";
var mockConfiguration = new Mock<IConfiguration>();
mockConfiguration.Setup(c => c["ASPNETCORE_ENVIRONMENT"])
.Returns((string?)null);
mockConfiguration.Setup(c => c["DOTNET_ENVIRONMENT"])
.Returns((string?)null);

// Act
var result = Utility.GetCurrentEnvironment(mockConfiguration.Object);

// Assert
Assert.Equal(expectedDefaultEnvironment, result);
}

[Fact]
public void GetCurrentEnvironment_WithEmptyEnvironmentVariables_ReturnsDefaultDevelopment()
{
// Arrange
const string expectedDefaultEnvironment = "Development";
var mockConfiguration = new Mock<IConfiguration>();
mockConfiguration.Setup(c => c["ASPNETCORE_ENVIRONMENT"])
.Returns(string.Empty);
mockConfiguration.Setup(c => c["DOTNET_ENVIRONMENT"])
.Returns(string.Empty);

// Act
var result = Utility.GetCurrentEnvironment(mockConfiguration.Object);

// Assert
Assert.Equal(expectedDefaultEnvironment, result);
}

#endregion

#region GetAppIdFromToken Tests

[Fact]
public void GetAppIdFromToken_WithValidTokenContainingAppId_ReturnsAppId()
{
// Arrange
const string expectedAppId = "12345678-1234-1234-1234-123456789abc";
var token = CreateJwtToken(new Dictionary<string, object>
{
{ "appid", expectedAppId },
{ "aud", "test-audience" }
});

// Act
var result = Utility.GetAppIdFromToken(token);

// Assert
Assert.Equal(expectedAppId, result);
}

[Fact]
public void GetAppIdFromToken_WithValidTokenContainingAzp_ReturnsAzp()
{
// Arrange
const string expectedAppId = "87654321-4321-4321-4321-cba987654321";
var token = CreateJwtToken(new Dictionary<string, object>
{
{ "azp", expectedAppId },
{ "aud", "test-audience" }
});

// Act
var result = Utility.GetAppIdFromToken(token);

// Assert
Assert.Equal(expectedAppId, result);
}

[Fact]
public void GetAppIdFromToken_WithBothAppIdAndAzp_PrioritizesAppId()
{
// Arrange
const string appIdValue = "appid-value";
const string azpValue = "azp-value";
var token = CreateJwtToken(new Dictionary<string, object>
{
{ "appid", appIdValue },
{ "azp", azpValue },
{ "aud", "test-audience" }
});

// Act
var result = Utility.GetAppIdFromToken(token);

// Assert
Assert.Equal(appIdValue, result);
}

[Fact]
public void GetAppIdFromToken_WithNoAppIdOrAzp_ReturnsEmptyString()
{
// Arrange
var token = CreateJwtToken(new Dictionary<string, object>
{
{ "aud", "test-audience" },
{ "sub", "test-subject" }
});

// Act
var result = Utility.GetAppIdFromToken(token);

// Assert
Assert.Equal(string.Empty, result);
}

[Fact]
public void GetAppIdFromToken_WithNullToken_ReturnsEmptyGuid()
{
// Act
var result = Utility.GetAppIdFromToken(null);

// Assert
Assert.Equal(Guid.Empty.ToString(), result);
}

[Fact]
public void GetAppIdFromToken_WithEmptyToken_ReturnsEmptyGuid()
{
// Act
var result = Utility.GetAppIdFromToken(string.Empty);

// Assert
Assert.Equal(Guid.Empty.ToString(), result);
}

[Fact]
public void GetAppIdFromToken_WithWhitespaceToken_ReturnsEmptyGuid()
{
// Act
var result = Utility.GetAppIdFromToken(" ");

// Assert
Assert.Equal(Guid.Empty.ToString(), result);
}

[Theory]
[InlineData("invalid-token")]
[InlineData("not.a.jwt")]
[InlineData("header.payload")]
public void GetAppIdFromToken_WithInvalidToken_ThrowsException(string invalidToken)
{
// Act & Assert
Assert.Throws<ArgumentException>(() => Utility.GetAppIdFromToken(invalidToken));
}

#endregion

#region ResolveAgentIdentity Tests

[Fact]
public void ResolveAgentIdentity_WithAgenticRequest_ReturnsAgenticInstanceId()
{
// Arrange
const string expectedAgenticInstanceId = "agentic-instance-123";
const string authToken = "dummy-token";

var mockTurnContext = new Mock<ITurnContext>();
var mockActivity = new Mock<IActivity>();

mockTurnContext.Setup(tc => tc.Activity).Returns(mockActivity.Object);
mockActivity.Setup(a => a.IsAgenticRequest()).Returns(true);
mockActivity.Setup(a => a.GetAgenticInstanceId()).Returns(expectedAgenticInstanceId);

// Act
var result = Utility.ResolveAgentIdentity(mockTurnContext.Object, authToken);

// Assert
Assert.Equal(expectedAgenticInstanceId, result);
mockActivity.Verify(a => a.IsAgenticRequest(), Times.Once);
mockActivity.Verify(a => a.GetAgenticInstanceId(), Times.Once);
}

[Fact]
public void ResolveAgentIdentity_WithNonAgenticRequest_ReturnsAppIdFromToken()
{
// Arrange
const string expectedAppId = "token-app-id-456";
var authToken = CreateJwtToken(new Dictionary<string, object>
{
{ "appid", expectedAppId }
});

var mockTurnContext = new Mock<ITurnContext>();
var mockActivity = new Mock<IActivity>();

mockTurnContext.Setup(tc => tc.Activity).Returns(mockActivity.Object);
mockActivity.Setup(a => a.IsAgenticRequest()).Returns(false);

// Act
var result = Utility.ResolveAgentIdentity(mockTurnContext.Object, authToken);

// Assert
Assert.Equal(expectedAppId, result);
mockActivity.Verify(a => a.IsAgenticRequest(), Times.Once);
mockActivity.Verify(a => a.GetAgenticInstanceId(), Times.Never);
}

[Fact]
public void ResolveAgentIdentity_WithNonAgenticRequestAndInvalidToken_ReturnsEmptyString()
{
// Arrange
const string authToken = "invalid-token";

var mockTurnContext = new Mock<ITurnContext>();
var mockActivity = new Mock<IActivity>();

mockTurnContext.Setup(tc => tc.Activity).Returns(mockActivity.Object);
mockActivity.Setup(a => a.IsAgenticRequest()).Returns(false);

// Act & Assert
Assert.Throws<ArgumentException>(() => Utility.ResolveAgentIdentity(mockTurnContext.Object, authToken));
}

[Fact]
public void ResolveAgentIdentity_WithNonAgenticRequestAndNullToken_ReturnsEmptyGuid()
{
// Arrange
string? authToken = null;

var mockTurnContext = new Mock<ITurnContext>();
var mockActivity = new Mock<IActivity>();

mockTurnContext.Setup(tc => tc.Activity).Returns(mockActivity.Object);
mockActivity.Setup(a => a.IsAgenticRequest()).Returns(false);

// Act
var result = Utility.ResolveAgentIdentity(mockTurnContext.Object, authToken);

// Assert
Assert.Equal(Guid.Empty.ToString(), result);
}

#endregion

#region Helper Methods

/// <summary>
/// Creates a JWT token with the specified claims for testing purposes.
/// </summary>
/// <param name="claims">Dictionary of claims to include in the token.</param>
/// <returns>A JWT token string.</returns>
private static string CreateJwtToken(Dictionary<string, object> claims)
{
var handler = new JwtSecurityTokenHandler();
var claimsList = claims.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()!)).ToList();

var tokenDescriptor = new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claimsList),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(
new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("test-key-that-is-long-enough-for-hmac-sha256")),
Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256Signature)
};

var token = handler.CreateToken(tokenDescriptor);
return handler.WriteToken(token);
}

#endregion
}
} No newline at end of file
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire test file appears to be in an incorrect directory (src/src/Tests/Runtime.Tests/). The correct location should be src/Tests/Runtime.Tests/ where the project file exists. Additionally, this file appears to be a duplicate or alternative version of the file in src/Tests/Runtime.Tests/UtilityTests.cs (179 lines), creating confusion about which is the authoritative version.

Please remove this file from src/src/Tests/Runtime.Tests/ and consolidate all tests into src/Tests/Runtime.Tests/UtilityTests.cs.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 200
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Agents.A365.Runtime.Authentication;
using Microsoft.Agents.A365.Runtime.Utils;
using Microsoft.Agents.Builder;
using Microsoft.Agents.Builder.App.UserAuth;
using Microsoft.Extensions.Configuration;
using Moq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.Agents.A365.Runtime.Tests
{
/// <summary>
/// Unit tests for AgenticAuthenticationService class.
/// Tests the authentication token retrieval functionality for agentic users.
/// </summary>
public class AgenticAuthenticationServiceTests
{
[Fact]
public async Task GetAgenticUserTokenAsync_WithValidParameters_ReturnsToken()
{
// Arrange
const string expectedToken = "test-token-123";
const string authHandlerName = "test-handler";
const string testScope = "test-scope";

var mockUserAuthorization = new Mock<UserAuthorization>();
var mockTurnContext = new Mock<ITurnContext>();
var mockConfiguration = new Mock<IConfiguration>();

mockConfiguration.Setup(c => c["MCP_PLATFORM_AUTHENTICATION_SCOPE"])
.Returns(testScope);

mockUserAuthorization
.Setup(ua => ua.ExchangeTurnTokenAsync(
It.IsAny<ITurnContext>(),
It.Is<string>(s => s == authHandlerName),
It.Is<List<string>>(scopes => scopes.Count == 1 && scopes[0] == testScope)))
.ReturnsAsync(expectedToken);

// Act
var result = await AgenticAuthenticationService.GetAgenticUserTokenAsync(
mockUserAuthorization.Object,
authHandlerName,
mockTurnContext.Object,
mockConfiguration.Object);

// Assert
Assert.Equal(expectedToken, result);
mockUserAuthorization.Verify(ua => ua.ExchangeTurnTokenAsync(
mockTurnContext.Object,
authHandlerName,
It.Is<List<string>>(scopes => scopes.Count == 1 && scopes[0] == testScope)),
Times.Once);
}

[Fact]
public async Task GetAgenticUserTokenAsync_WithNullConfiguration_UsesDefaultScope()
{
// Arrange
const string expectedToken = "test-token-456";
const string authHandlerName = "test-handler";
const string defaultScope = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default";

var mockUserAuthorization = new Mock<UserAuthorization>();
var mockTurnContext = new Mock<ITurnContext>();
var mockConfiguration = new Mock<IConfiguration>();

mockConfiguration.Setup(c => c["MCP_PLATFORM_AUTHENTICATION_SCOPE"])
.Returns((string?)null);

mockUserAuthorization
.Setup(ua => ua.ExchangeTurnTokenAsync(
It.IsAny<ITurnContext>(),
It.Is<string>(s => s == authHandlerName),
It.Is<List<string>>(scopes => scopes.Count == 1 && scopes[0] == defaultScope)))
.ReturnsAsync(expectedToken);

// Act
var result = await AgenticAuthenticationService.GetAgenticUserTokenAsync(
mockUserAuthorization.Object,
authHandlerName,
mockTurnContext.Object,
mockConfiguration.Object);

// Assert
Assert.Equal(expectedToken, result);
mockUserAuthorization.Verify(ua => ua.ExchangeTurnTokenAsync(
mockTurnContext.Object,
authHandlerName,
It.Is<List<string>>(scopes => scopes.Count == 1 && scopes[0] == defaultScope)),
Times.Once);
}

[Fact]
public async Task GetAgenticUserTokenAsync_WithEmptyAuthHandlerName_PassesEmptyString()
{
// Arrange
const string expectedToken = "test-token-789";
const string authHandlerName = "";
const string testScope = "test-scope";

var mockUserAuthorization = new Mock<UserAuthorization>();
var mockTurnContext = new Mock<ITurnContext>();
var mockConfiguration = new Mock<IConfiguration>();

mockConfiguration.Setup(c => c["MCP_PLATFORM_AUTHENTICATION_SCOPE"])
.Returns(testScope);

mockUserAuthorization
.Setup(ua => ua.ExchangeTurnTokenAsync(
It.IsAny<ITurnContext>(),
It.Is<string>(s => s == authHandlerName),
It.Is<List<string>>(scopes => scopes.Count == 1 && scopes[0] == testScope)))
.ReturnsAsync(expectedToken);

// Act
var result = await AgenticAuthenticationService.GetAgenticUserTokenAsync(
mockUserAuthorization.Object,
authHandlerName,
mockTurnContext.Object,
mockConfiguration.Object);

// Assert
Assert.Equal(expectedToken, result);
}

[Fact]
public async Task GetAgenticUserTokenAsync_WhenUserAuthorizationThrows_PropagatesException()
{
// Arrange
const string authHandlerName = "test-handler";
var expectedException = new InvalidOperationException("Test exception");

var mockUserAuthorization = new Mock<UserAuthorization>();
var mockTurnContext = new Mock<ITurnContext>();
var mockConfiguration = new Mock<IConfiguration>();

mockConfiguration.Setup(c => c["MCP_PLATFORM_AUTHENTICATION_SCOPE"])
.Returns("test-scope");

mockUserAuthorization
.Setup(ua => ua.ExchangeTurnTokenAsync(
It.IsAny<ITurnContext>(),
It.IsAny<string>(),
It.IsAny<List<string>>()))
.ThrowsAsync(expectedException);

// Act & Assert
var actualException = await Assert.ThrowsAsync<InvalidOperationException>(
() => AgenticAuthenticationService.GetAgenticUserTokenAsync(
mockUserAuthorization.Object,
authHandlerName,
mockTurnContext.Object,
mockConfiguration.Object));

Assert.Same(expectedException, actualException);
}

[Fact]
public async Task GetAgenticUserTokenAsync_CallsUtilityGetMcpPlatformAuthenticationScope()
{
// Arrange
const string expectedToken = "test-token-utility";
const string authHandlerName = "test-handler";
const string testScope = "custom-scope-from-utility";

var mockUserAuthorization = new Mock<UserAuthorization>();
var mockTurnContext = new Mock<ITurnContext>();
var mockConfiguration = new Mock<IConfiguration>();

mockConfiguration.Setup(c => c["MCP_PLATFORM_AUTHENTICATION_SCOPE"])
.Returns(testScope);

mockUserAuthorization
.Setup(ua => ua.ExchangeTurnTokenAsync(
It.IsAny<ITurnContext>(),
It.IsAny<string>(),
It.Is<List<string>>(scopes => scopes.Count == 1 && scopes[0] == testScope)))
.ReturnsAsync(expectedToken);

// Act
var result = await AgenticAuthenticationService.GetAgenticUserTokenAsync(
mockUserAuthorization.Object,
authHandlerName,
mockTurnContext.Object,
mockConfiguration.Object);

// Assert
Assert.Equal(expectedToken, result);

// Verify that the scope passed matches what Utility.GetMcpPlatformAuthenticationScope would return
mockConfiguration.Verify(c => c["MCP_PLATFORM_AUTHENTICATION_SCOPE"], Times.Once);
}
}
} No newline at end of file
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file is located in the wrong directory (src/src/Tests/Runtime.Tests/). The correct location should be src/Tests/Runtime.Tests/ where the project file exists. Additionally, there appears to be a duplicate or alternative version of this file in src/Tests/Runtime.Tests/AgenticAuthenticationServiceTests.cs (118 lines).

Please remove this file from src/src/Tests/Runtime.Tests/ and consolidate all tests into src/Tests/Runtime.Tests/AgenticAuthenticationServiceTests.cs.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants