diff --git a/.gitignore b/.gitignore index c0b11341..81c6ce11 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,14 @@ CodeCoverage/ [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* +# Test Coverage +coverage/ +coverage-report/ +coverage-report-all/ +TestResults/ +*.cobertura.xml +*.trx + # Visual Studio .vs/ diff --git a/src/Tests/README.md b/src/Tests/README.md new file mode 100644 index 00000000..27408850 --- /dev/null +++ b/src/Tests/README.md @@ -0,0 +1,27 @@ +# Microsoft Agent 365 SDK Tests + +Unit and integration tests for the Microsoft Agent 365 .NET SDK. This test suite ensures reliability, maintainability, and quality across all modules including runtime, tooling, notifications, and observability extensions. + +## Usage + +For detailed instructions on running tests and generating coverage reports, see: + +- [Test Plan](TEST_PLAN.md) - Comprehensive testing strategy and implementation roadmap +- [Running Tests](RUNNING_TESTS.md) - Complete guide for installation, running tests, generating coverage reports, and troubleshooting + +## Support + +For issues, questions, or feedback: + +- File issues in the [GitHub Issues](https://github.com/microsoft/Agent365-dotnet/issues) section +- See the [main documentation](../README.md) for more information + +## Trademarks + +Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services referenced in the documentation may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries. The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks. Microsoft's general trademark guidelines can be found at http://go.microsoft.com/fwlink/?LinkID=254653. + +## License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the MIT License - see the [LICENSE](../LICENSE) file for details. \ No newline at end of file diff --git a/src/Tests/RUNNING_TESTS.md b/src/Tests/RUNNING_TESTS.md new file mode 100644 index 00000000..22dc8f1f --- /dev/null +++ b/src/Tests/RUNNING_TESTS.md @@ -0,0 +1,147 @@ +# Running Unit Tests for Agent365-dotnet SDK + +This guide covers setting up and running tests. + +--- + +## Prerequisites + +### 1. Install .NET SDK + +Ensure you have .NET 8 SDK or later installed: + +```powershell +# Verify installation +dotnet --version # Should be 8.0.0 or later +``` + +### 2. Restore Dependencies + +```powershell +# From repository root +dotnet restore + +# Or from src directory +cd src +dotnet restore +``` + +--- + +## Test Structure + +> **Note:** This structure will be updated as new tests are added. + +```plaintext +Tests/ +├── Runtime.Tests/ # Runtime core tests +├── Microsoft.Agents.A365.Observability.Runtime.Tests/ # Observability runtime tests +├── Microsoft.Agents.A365.Observability.Hosting.Tests/ # Observability hosting tests +├── Microsoft.Agents.A365.Observability.Extension.Tests/ # Observability extension tests +└── Microsoft.Agents.A365.Notifications.Tests/ # Notifications tests +``` + +--- + +## Running Tests in VS Code (Optional) + +### Test Explorer + +1. Install **C# Dev Kit** extension +2. Click the beaker icon in the Activity Bar or press `Ctrl+Shift+P` → "Test: Focus on Test Explorer View" +3. Click the play button to run tests (all/folder/file/individual) +4. Right-click → "Debug Test" to debug with breakpoints + +### Command Palette + +- `Test: Run All Tests` +- `Test: Run Tests in Current File` +- `Test: Debug Tests in Current File` + +--- + +## Running Tests from Command Line + +```powershell +# Run all tests +dotnet test + +# Run specific module/file +dotnet test Tests/Runtime.Tests/ +dotnet test Tests/Runtime.Tests/Microsoft.Agents.A365.Runtime.Tests.csproj + +# Run with options +dotnet test --verbosity detailed # Verbose +dotnet test --filter "FullyQualifiedName~Utility" # Pattern matching +dotnet test --logger "console;verbosity=detailed" # Detailed logging +``` + +--- + +## Generating Reports + +### HTML Reports + +```powershell +# Install coverage tools (one-time) +dotnet tool install --global dotnet-reportgenerator-globaltool + +# Generate coverage report +dotnet test --collect:"XPlat Code Coverage" --results-directory ./coverage + +# Generate HTML report +reportgenerator -reports:"./coverage/**/coverage.cobertura.xml" -targetdir:"./coverage-report" -reporttypes:Html + +# View reports +start ./coverage-report/index.html +``` + +### CI/CD Reports + +```powershell +# XML reports for CI/CD pipelines +dotnet test --logger "trx;LogFileName=test-results.trx" --collect:"XPlat Code Coverage" + +# View reports +start TestResults/test-results.trx +``` + +--- + +## Troubleshooting + +### Common Issues + +| Issue | Solution | +|-------|----------| +| **Test loading failed** | Clean and rebuild: `dotnet clean`, then `dotnet build` | +| **Tests not discovered** | Verify `[Fact]` or `[Theory]` attributes, refresh Test Explorer | +| **Build failures** | Run `dotnet restore`, check package references | +| **Coverage not generated** | Verify `coverlet.collector` package is referenced | + +### Fix Steps + +If tests fail to discover or build errors occur: + +**1. Clean and Rebuild** + +```powershell +dotnet clean +dotnet restore +dotnet build +``` + +**2. Clear Test Cache** + +```powershell +Remove-Item -Recurse -Force bin, obj +dotnet restore +dotnet build +dotnet test +``` + +**3. Restart VS Code** + +- Close completely and reopen +- Wait for C# extension to reload +- Refresh Test Explorer diff --git a/src/Tests/Runtime.Tests/AgenticAuthenticationServiceTests.cs b/src/Tests/Runtime.Tests/AgenticAuthenticationServiceTests.cs new file mode 100644 index 00000000..2a94ae1b --- /dev/null +++ b/src/Tests/Runtime.Tests/AgenticAuthenticationServiceTests.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Agents.A365.Runtime.Utils; +using Microsoft.Extensions.Configuration; +using Moq; +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Agents.A365.Runtime.Tests +{ + /// + /// Unit tests for AgenticAuthenticationService class. + /// Tests the authentication token retrieval and utility methods. + /// + public class AgenticAuthenticationServiceTests + { + [Theory] + [InlineData("custom-scope", "custom-scope")] // Custom scope + [InlineData("SKIP", "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default")] // Null returns default + [InlineData("", "")] // Empty string + public void GetMcpPlatformAuthenticationScope_WithVariousConfigurations_ReturnsExpectedScope( + string? configValue, + string expectedScope) + { + // Arrange + var mockConfiguration = new Mock(); + mockConfiguration.Setup(c => c["MCP_PLATFORM_AUTHENTICATION_SCOPE"]) + .Returns(configValue == "SKIP" ? null : configValue); + + // Act + var result = Utility.GetMcpPlatformAuthenticationScope(mockConfiguration.Object); + + // Assert + Assert.Equal(expectedScope, result); + } + + [Theory] + [InlineData("Production", "SKIP", "Production")] // ASPNETCORE_ENVIRONMENT takes precedence + [InlineData("SKIP", "Development", "Development")] // Falls back to DOTNET_ENVIRONMENT + [InlineData("SKIP", "SKIP", "Development")] // Both null returns default + public void GetCurrentEnvironment_WithVariousConfigurations_ReturnsExpectedEnvironment( + string? aspNetCoreEnv, + string? dotNetEnv, + string expectedEnvironment) + { + // Arrange + var mockConfiguration = new Mock(); + mockConfiguration.Setup(c => c["ASPNETCORE_ENVIRONMENT"]) + .Returns(aspNetCoreEnv == "SKIP" ? null : aspNetCoreEnv); + mockConfiguration.Setup(c => c["DOTNET_ENVIRONMENT"]) + .Returns(dotNetEnv == "SKIP" ? null : dotNetEnv); + + // Act + var result = Utility.GetCurrentEnvironment(mockConfiguration.Object); + + // Assert + Assert.Equal(expectedEnvironment, result); + } + + [Fact] + public void GetMcpPlatformAuthenticationScope_ConfigurationReturnsNull_ReturnsDefaultScope() + { + // Arrange + var mockConfiguration = new Mock(); + mockConfiguration.Setup(c => c["MCP_PLATFORM_AUTHENTICATION_SCOPE"]) + .Returns((string?)null); + + // Act + var result = Utility.GetMcpPlatformAuthenticationScope(mockConfiguration.Object); + + // Assert + Assert.Equal("ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default", result); + } + + [Theory] + [InlineData("scope1", 1)] + [InlineData("scope2", 3)] + public void GetMcpPlatformAuthenticationScope_CalledMultipleTimes_AccessesConfigurationCorrectly( + string testScope, + int callCount) + { + // Arrange + var mockConfiguration = new Mock(); + 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)); + } + } +} diff --git a/src/Tests/Runtime.Tests/TenantContextHelperTests.cs b/src/Tests/Runtime.Tests/TenantContextHelperTests.cs index 0b76f481..a43880cd 100644 --- a/src/Tests/Runtime.Tests/TenantContextHelperTests.cs +++ b/src/Tests/Runtime.Tests/TenantContextHelperTests.cs @@ -1,11 +1,14 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; -using Microsoft.Agents.A365.Runtime.Common; -using Moq; -using System.Security.Claims; -using Xunit; - -namespace Microsoft.Agents.A365.Runtime.Common.Tests +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Agents.A365.Runtime; +using Moq; +using System.Security.Claims; +using Xunit; + +namespace Microsoft.Agents.A365.Runtime.Tests { /// /// Unit tests for TenantContextHelper class. @@ -29,460 +32,176 @@ public void Constants_ShouldHaveExpectedValues() #endregion - #region GetTenantId Tests - - [Fact] - public void GetTenantId_WhenContextIsNull_ReturnsNull() - { - // Arrange - HttpContext? context = null; - - // Act - var result = TenantContextHelper.GetTenantId(context); - - // Assert - Assert.Null(result); - } - - [Fact] - public void GetTenantId_WhenTenantInClaims_ReturnsTenantFromClaims() - { - // Arrange - const string expectedTenantId = "tenant-123"; - var context = CreateMockHttpContext(); - var claims = new List - { - new(TenantContextHelper.TenantClaimName, expectedTenantId) - }; - var principal = new ClaimsPrincipal(new ClaimsIdentity(claims)); - context.User = principal; - - // Act - var result = TenantContextHelper.GetTenantId(context); - - // Assert - Assert.Equal(expectedTenantId, result); - } - - [Fact] - public void GetTenantId_WhenTenantInHeaders_ReturnsTenantFromHeaders() - { - // Arrange - const string expectedTenantId = "tenant-456"; - var context = CreateMockHttpContext(); - context.Request.Headers[TenantContextHelper.TenantHeaderName] = expectedTenantId; - - // Act - var result = TenantContextHelper.GetTenantId(context); - - // Assert - Assert.Equal(expectedTenantId, result); - } - - [Fact] - public void GetTenantId_WhenTenantInItems_ReturnsTenantFromItems() - { - // Arrange - const string expectedTenantId = "tenant-789"; - var context = CreateMockHttpContext(); - context.Items[TenantContextHelper.TenantItemKey] = expectedTenantId; - - // Act - var result = TenantContextHelper.GetTenantId(context); - - // Assert - Assert.Equal(expectedTenantId, result); - } - - [Fact] - public void GetTenantId_WhenTenantInMultipleSources_PrioritizesClaims() - { - // Arrange - const string tenantFromClaims = "tenant-claims"; - const string tenantFromHeaders = "tenant-headers"; - const string tenantFromItems = "tenant-items"; - - var context = CreateMockHttpContext(); - - // Set up claims - var claims = new List - { - new(TenantContextHelper.TenantClaimName, tenantFromClaims) - }; - var principal = new ClaimsPrincipal(new ClaimsIdentity(claims)); - context.User = principal; - - // Set up headers - context.Request.Headers[TenantContextHelper.TenantHeaderName] = tenantFromHeaders; - - // Set up items - context.Items[TenantContextHelper.TenantItemKey] = tenantFromItems; - - // Act - var result = TenantContextHelper.GetTenantId(context); - - // Assert - Assert.Equal(tenantFromClaims, result); - } - - [Fact] - public void GetTenantId_WhenTenantInHeadersAndItems_PrioritizesHeaders() - { - // Arrange - const string tenantFromHeaders = "tenant-headers"; - const string tenantFromItems = "tenant-items"; - - var context = CreateMockHttpContext(); - - // Set up headers - context.Request.Headers[TenantContextHelper.TenantHeaderName] = tenantFromHeaders; - - // Set up items - context.Items[TenantContextHelper.TenantItemKey] = tenantFromItems; - - // Act - var result = TenantContextHelper.GetTenantId(context); - - // Assert - Assert.Equal(tenantFromHeaders, result); - } - - [Fact] - public void GetTenantId_WhenTenantClaimIsEmpty_FallsBackToHeaders() - { - // Arrange - const string tenantFromHeaders = "tenant-headers"; - var context = CreateMockHttpContext(); - - // Set up empty claim - var claims = new List - { - new(TenantContextHelper.TenantClaimName, "") - }; - var principal = new ClaimsPrincipal(new ClaimsIdentity(claims)); - context.User = principal; - - // Set up headers - context.Request.Headers[TenantContextHelper.TenantHeaderName] = tenantFromHeaders; - - // Act - var result = TenantContextHelper.GetTenantId(context); - - // Assert - Assert.Equal(tenantFromHeaders, result); - } - - [Fact] - public void GetTenantId_WhenTenantClaimIsWhitespace_FallsBackToHeaders() - { - // Arrange - const string tenantFromHeaders = "tenant-headers"; - var context = CreateMockHttpContext(); - - // Set up whitespace claim - var claims = new List - { - new(TenantContextHelper.TenantClaimName, " ") - }; - var principal = new ClaimsPrincipal(new ClaimsIdentity(claims)); - context.User = principal; - - // Set up headers - context.Request.Headers[TenantContextHelper.TenantHeaderName] = tenantFromHeaders; - - // Act - var result = TenantContextHelper.GetTenantId(context); - - // Assert - Assert.Equal(tenantFromHeaders, result); - } - - [Fact] - public void GetTenantId_WhenNoTenantFound_ReturnsNull() - { - // Arrange - var context = CreateMockHttpContext(); - - // Act - var result = TenantContextHelper.GetTenantId(context); - - // Assert - Assert.Null(result); - } - - [Fact] - public void GetTenantId_WhenUserIsNull_FallsBackToHeaders() - { - // Arrange - const string tenantFromHeaders = "tenant-headers"; - var context = CreateMockHttpContext(); - context.User = new ClaimsPrincipal(); - context.Request.Headers[TenantContextHelper.TenantHeaderName] = tenantFromHeaders; - - // Act - var result = TenantContextHelper.GetTenantId(context); - - // Assert - Assert.Equal(tenantFromHeaders, result); - } - + #region GetTenantId Tests + + [Fact] + public void GetTenantId_WhenContextIsNull_ReturnsNull() + { + // Arrange + HttpContext? context = null; + + // Act + var result = TenantContextHelper.GetTenantId(context); + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("tenant-claims", null, null, "tenant-claims")] // Claims only + [InlineData(null, "tenant-headers", null, "tenant-headers")] // Headers only + [InlineData(null, null, "tenant-items", "tenant-items")] // Items only + [InlineData("tenant-claims", "tenant-headers", "tenant-items", "tenant-claims")] // Claims has priority + [InlineData(null, "tenant-headers", "tenant-items", "tenant-headers")] // Headers has priority over items + [InlineData("", "tenant-headers", null, "tenant-headers")] // Empty claim falls back to headers + [InlineData(" ", "tenant-headers", null, "tenant-headers")] // Whitespace claim falls back to headers + public void GetTenantId_WithVariousSources_ReturnsExpectedTenantId( + string? claimValue, + string? headerValue, + string? itemValue, + string? expectedTenantId) + { + // Arrange + var context = CreateMockHttpContext(); + + if (claimValue != null) + { + var claims = new List { new(TenantContextHelper.TenantClaimName, claimValue) }; + context.User = new ClaimsPrincipal(new ClaimsIdentity(claims)); + } + + if (headerValue != null) + { + context.Request.Headers[TenantContextHelper.TenantHeaderName] = headerValue; + } + + if (itemValue != null) + { + context.Items[TenantContextHelper.TenantItemKey] = itemValue; + } + + // Act + var result = TenantContextHelper.GetTenantId(context); + + // Assert + Assert.Equal(expectedTenantId, result); + } + + [Theory] + [InlineData(12345, "12345")] // Integer to string + [InlineData(null, null)] // Null item + public void GetTenantId_WithVariousItemTypes_ReturnsExpectedResult(object? itemValue, string? expectedResult) + { + // Arrange + var context = CreateMockHttpContext(); + context.Items[TenantContextHelper.TenantItemKey] = itemValue; + + // Act + var result = TenantContextHelper.GetTenantId(context); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void GetTenantId_WhenNoTenantFound_ReturnsNull() + { + // Arrange + var context = CreateMockHttpContext(); + + // Act + var result = TenantContextHelper.GetTenantId(context); + + // Assert + Assert.Null(result); + } + #endregion - #region GetWorkerId Tests - - [Fact] - public void GetWorkerId_WhenContextIsNull_ReturnsNull() - { - // Arrange - HttpContext? context = null; - - // Act - var result = TenantContextHelper.GetWorkerId(context); - - // Assert - Assert.Null(result); - } - - [Fact] - public void GetWorkerId_WhenWorkerInClaims_ReturnsWorkerFromClaims() - { - // Arrange - const string expectedWorkerId = "worker-123"; - var context = CreateMockHttpContext(); - var claims = new List - { - new(TenantContextHelper.WorkerClaimName, expectedWorkerId) - }; - var principal = new ClaimsPrincipal(new ClaimsIdentity(claims)); - context.User = principal; - - // Act - var result = TenantContextHelper.GetWorkerId(context); - - // Assert - Assert.Equal(expectedWorkerId, result); - } - - [Fact] - public void GetWorkerId_WhenWorkerInHeaders_ReturnsWorkerFromHeaders() - { - // Arrange - const string expectedWorkerId = "worker-456"; - var context = CreateMockHttpContext(); - context.Request.Headers[TenantContextHelper.WorkerHeaderName] = expectedWorkerId; - - // Act - var result = TenantContextHelper.GetWorkerId(context); - - // Assert - Assert.Equal(expectedWorkerId, result); - } - - [Fact] - public void GetWorkerId_WhenWorkerInItems_ReturnsWorkerFromItems() - { - // Arrange - const string expectedWorkerId = "worker-789"; - var context = CreateMockHttpContext(); - context.Items[TenantContextHelper.WorkerItemKey] = expectedWorkerId; - - // Act - var result = TenantContextHelper.GetWorkerId(context); - - // Assert - Assert.Equal(expectedWorkerId, result); - } - - [Fact] - public void GetWorkerId_WhenWorkerInMultipleSources_PrioritizesClaims() - { - // Arrange - const string workerFromClaims = "worker-claims"; - const string workerFromHeaders = "worker-headers"; - const string workerFromItems = "worker-items"; - - var context = CreateMockHttpContext(); - - // Set up claims - var claims = new List - { - new(TenantContextHelper.WorkerClaimName, workerFromClaims) - }; - var principal = new ClaimsPrincipal(new ClaimsIdentity(claims)); - context.User = principal; - - // Set up headers - context.Request.Headers[TenantContextHelper.WorkerHeaderName] = workerFromHeaders; - - // Set up items - context.Items[TenantContextHelper.WorkerItemKey] = workerFromItems; - - // Act - var result = TenantContextHelper.GetWorkerId(context); - - // Assert - Assert.Equal(workerFromClaims, result); - } - - [Fact] - public void GetWorkerId_WhenWorkerInHeadersAndItems_PrioritizesHeaders() - { - // Arrange - const string workerFromHeaders = "worker-headers"; - const string workerFromItems = "worker-items"; - - var context = CreateMockHttpContext(); - - // Set up headers - context.Request.Headers[TenantContextHelper.WorkerHeaderName] = workerFromHeaders; - - // Set up items - context.Items[TenantContextHelper.WorkerItemKey] = workerFromItems; - - // Act - var result = TenantContextHelper.GetWorkerId(context); - - // Assert - Assert.Equal(workerFromHeaders, result); - } - - [Fact] - public void GetWorkerId_WhenWorkerClaimIsEmpty_FallsBackToHeaders() - { - // Arrange - const string workerFromHeaders = "worker-headers"; - var context = CreateMockHttpContext(); - - // Set up empty claim - var claims = new List - { - new(TenantContextHelper.WorkerClaimName, "") - }; - var principal = new ClaimsPrincipal(new ClaimsIdentity(claims)); - context.User = principal; - - // Set up headers - context.Request.Headers[TenantContextHelper.WorkerHeaderName] = workerFromHeaders; - - // Act - var result = TenantContextHelper.GetWorkerId(context); - - // Assert - Assert.Equal(workerFromHeaders, result); - } - - [Fact] - public void GetWorkerId_WhenWorkerClaimIsWhitespace_FallsBackToHeaders() - { - // Arrange - const string workerFromHeaders = "worker-headers"; - var context = CreateMockHttpContext(); - - // Set up whitespace claim - var claims = new List - { - new(TenantContextHelper.WorkerClaimName, " ") - }; - var principal = new ClaimsPrincipal(new ClaimsIdentity(claims)); - context.User = principal; - - // Set up headers - context.Request.Headers[TenantContextHelper.WorkerHeaderName] = workerFromHeaders; - - // Act - var result = TenantContextHelper.GetWorkerId(context); - - // Assert - Assert.Equal(workerFromHeaders, result); - } - - [Fact] - public void GetWorkerId_WhenNoWorkerFound_ReturnsNull() - { - // Arrange - var context = CreateMockHttpContext(); - - // Act - var result = TenantContextHelper.GetWorkerId(context); - - // Assert - Assert.Null(result); - } - - [Fact] - public void GetWorkerId_WhenUserIsNull_FallsBackToHeaders() - { - // Arrange - const string workerFromHeaders = "worker-headers"; - var context = CreateMockHttpContext(); - context.User = new ClaimsPrincipal(); - context.Request.Headers[TenantContextHelper.WorkerHeaderName] = workerFromHeaders; - - // Act - var result = TenantContextHelper.GetWorkerId(context); - - // Assert - Assert.Equal(workerFromHeaders, result); - } - - [Fact] - public void GetWorkerId_WhenItemValueIsNonString_ReturnsStringRepresentation() - { - // Arrange - const int workerIdAsInt = 12345; - var context = CreateMockHttpContext(); - context.Items[TenantContextHelper.WorkerItemKey] = workerIdAsInt; - - // Act - var result = TenantContextHelper.GetWorkerId(context); - - // Assert - Assert.Equal("12345", result); - } - - [Fact] - public void GetTenantId_WhenItemValueIsNonString_ReturnsStringRepresentation() - { - // Arrange - const int tenantIdAsInt = 67890; - var context = CreateMockHttpContext(); - context.Items[TenantContextHelper.TenantItemKey] = tenantIdAsInt; - - // Act - var result = TenantContextHelper.GetTenantId(context); - - // Assert - Assert.Equal("67890", result); - } - - [Fact] - public void GetTenantId_WhenItemValueIsNull_ReturnsNull() - { - // Arrange - var context = CreateMockHttpContext(); - context.Items[TenantContextHelper.TenantItemKey] = null; - - // Act - var result = TenantContextHelper.GetTenantId(context); - - // Assert - Assert.Null(result); - } - - [Fact] - public void GetWorkerId_WhenItemValueIsNull_ReturnsNull() - { - // Arrange - var context = CreateMockHttpContext(); - context.Items[TenantContextHelper.WorkerItemKey] = null; - - // Act - var result = TenantContextHelper.GetWorkerId(context); - - // Assert - Assert.Null(result); - } - + #region GetWorkerId Tests + + [Fact] + public void GetWorkerId_WhenContextIsNull_ReturnsNull() + { + // Arrange + HttpContext? context = null; + + // Act + var result = TenantContextHelper.GetWorkerId(context); + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("worker-claims", null, null, "worker-claims")] // Claims only + [InlineData(null, "worker-headers", null, "worker-headers")] // Headers only + [InlineData(null, null, "worker-items", "worker-items")] // Items only + [InlineData("worker-claims", "worker-headers", "worker-items", "worker-claims")] // Claims has priority + [InlineData(null, "worker-headers", "worker-items", "worker-headers")] // Headers has priority over items + [InlineData("", "worker-headers", null, "worker-headers")] // Empty claim falls back to headers + [InlineData(" ", "worker-headers", null, "worker-headers")] // Whitespace claim falls back to headers + public void GetWorkerId_WithVariousSources_ReturnsExpectedWorkerId( + string? claimValue, + string? headerValue, + string? itemValue, + string? expectedWorkerId) + { + // Arrange + var context = CreateMockHttpContext(); + + if (claimValue != null) + { + var claims = new List { new(TenantContextHelper.WorkerClaimName, claimValue) }; + context.User = new ClaimsPrincipal(new ClaimsIdentity(claims)); + } + + if (headerValue != null) + { + context.Request.Headers[TenantContextHelper.WorkerHeaderName] = headerValue; + } + + if (itemValue != null) + { + context.Items[TenantContextHelper.WorkerItemKey] = itemValue; + } + + // Act + var result = TenantContextHelper.GetWorkerId(context); + + // Assert + Assert.Equal(expectedWorkerId, result); + } + + [Theory] + [InlineData(12345, "12345")] // Integer to string + [InlineData(null, null)] // Null item + public void GetWorkerId_WithVariousItemTypes_ReturnsExpectedResult(object? itemValue, string? expectedResult) + { + // Arrange + var context = CreateMockHttpContext(); + context.Items[TenantContextHelper.WorkerItemKey] = itemValue; + + // Act + var result = TenantContextHelper.GetWorkerId(context); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void GetWorkerId_WhenNoWorkerFound_ReturnsNull() + { + // Arrange + var context = CreateMockHttpContext(); + + // Act + var result = TenantContextHelper.GetWorkerId(context); + + // Assert + Assert.Null(result); + } + #endregion #region Helper Methods diff --git a/src/Tests/Runtime.Tests/UtilityTests.cs b/src/Tests/Runtime.Tests/UtilityTests.cs new file mode 100644 index 00000000..ad3d4550 --- /dev/null +++ b/src/Tests/Runtime.Tests/UtilityTests.cs @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Agents.A365.Runtime.Utils; +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 +{ + /// + /// Unit tests for Utility class. + /// Tests configuration retrieval, environment detection, and token handling utilities. + /// + public class UtilityTests + { + #region GetMcpPlatformAuthenticationScope Tests + + [Theory] + [InlineData("custom-scope", "custom-scope")] // Custom value returned as-is + [InlineData("SKIP", "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default")] // Null returns default + [InlineData("", "")] // Empty string returned as-is + public void GetMcpPlatformAuthenticationScope_WithVariousConfigurationValues_ReturnsExpectedScope( + string? configValue, + string expectedScope) + { + // Arrange + var mockConfiguration = new Mock(); + mockConfiguration.Setup(c => c["MCP_PLATFORM_AUTHENTICATION_SCOPE"]) + .Returns(configValue == "SKIP" ? null : configValue); + + // Act + var result = Utility.GetMcpPlatformAuthenticationScope(mockConfiguration.Object); + + // Assert + Assert.Equal(expectedScope, result); + } + + #endregion + + #region GetCurrentEnvironment Tests + + [Theory] + [InlineData("Production", "SKIP", "Production")] // ASPNETCORE_ENVIRONMENT takes precedence + [InlineData("SKIP", "Development", "Development")] // Falls back to DOTNET_ENVIRONMENT + [InlineData("SKIP", "SKIP", "Development")] // Both null returns default + [InlineData("", "", "")] // Empty strings returned as-is + public void GetCurrentEnvironment_WithVariousEnvironmentVariables_ReturnsExpectedEnvironment( + string? aspNetCoreEnv, + string? dotNetEnv, + string expectedEnvironment) + { + // Arrange + var mockConfiguration = new Mock(); + mockConfiguration.Setup(c => c["ASPNETCORE_ENVIRONMENT"]) + .Returns(aspNetCoreEnv == "SKIP" ? null : aspNetCoreEnv); + mockConfiguration.Setup(c => c["DOTNET_ENVIRONMENT"]) + .Returns(dotNetEnv == "SKIP" ? null : dotNetEnv); + + // Act + var result = Utility.GetCurrentEnvironment(mockConfiguration.Object); + + // Assert + Assert.Equal(expectedEnvironment, result); + } + + #endregion + + #region GetAppIdFromToken Tests + + [Theory] + [InlineData("12345678-1234-1234-1234-123456789abc", "SKIP", "12345678-1234-1234-1234-123456789abc")] // appid claim only + [InlineData("SKIP", "87654321-4321-4321-4321-cba987654321", "87654321-4321-4321-4321-cba987654321")] // azp claim only + [InlineData("appid-value", "azp-value", "appid-value")] // appid takes precedence over azp + [InlineData("SKIP", "SKIP", "")] // No claims returns empty string + public void GetAppIdFromToken_WithValidTokensContainingVariousClaims_ReturnsExpectedAppId( + string? appIdValue, + string? azpValue, + string expectedAppId) + { + // Arrange + var claims = new Dictionary { { "aud", "test-audience" } }; + + if (appIdValue != "SKIP" && appIdValue != null) + claims.Add("appid", appIdValue); + if (azpValue != "SKIP" && azpValue != null) + claims.Add("azp", azpValue); + + var token = CreateJwtToken(claims); + + // Act + var result = Utility.GetAppIdFromToken(token); + + // Assert + Assert.Equal(expectedAppId, result); + } + + [Theory] + [InlineData("")] // Empty string + [InlineData(" ")] // Whitespace + public void GetAppIdFromToken_WithNullOrWhitespaceToken_ReturnsEmptyGuid(string token) + { + // Act + var result = Utility.GetAppIdFromToken(token); + + // Assert + Assert.Equal(Guid.Empty.ToString(), result); + } + + [Fact] + public void GetAppIdFromToken_WithNullToken_ReturnsEmptyGuid() + { + // Act + var result = Utility.GetAppIdFromToken(null!); + + // Assert + Assert.Equal(Guid.Empty.ToString(), result); + } + + [Theory] + [InlineData("invalid-token")] // Invalid format + [InlineData("not.a.jwt")] // Wrong number of parts + public void GetAppIdFromToken_WithInvalidTokenFormats_ThrowsException(string invalidToken) + { + // Act & Assert + Assert.ThrowsAny(() => Utility.GetAppIdFromToken(invalidToken)); + } + + #endregion + + #region Helper Methods + + /// + /// Creates a JWT token with the specified claims for testing purposes. + /// + /// Dictionary of claims to include in the token. + /// A JWT token string. + private static string CreateJwtToken(Dictionary 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 + } +} diff --git a/src/Tests/TEST_PLAN.md b/src/Tests/TEST_PLAN.md new file mode 100644 index 00000000..b1ae0ae7 --- /dev/null +++ b/src/Tests/TEST_PLAN.md @@ -0,0 +1,169 @@ +# Test Plan for Agent365-dotnet SDK + +> **Note:** This plan is under active development. Keep updating as testing progresses. + +**Version:** 1.0 +**Date:** December 4, 2025 +**Status:** Draft + +--- + +## Overview + +### Current State +- ✅ Unit tests exist for `Runtime` core module (48 tests passing) +- ⏳ Partial tests for `Observability.Runtime` module +- ❌ Missing tests for `Tooling`, `Notifications`, and extension modules +- ❌ No integration tests or complete CI/CD automation + +### Goals +- Achieve **80%+ code coverage** across all modules +- Implement integration tests for cross-module functionality +- Integrate testing into CI/CD pipeline with coverage enforcement + +--- + +## Testing Strategy + +**Framework:** xUnit 2.8.2 +**Mocking:** Moq 4.20.72 +**Target Framework:** .NET 8 + +**Test Pattern:** AAA (Arrange → Act → Assert) +**Naming Convention:** `test___` + +--- + +## Implementation Roadmap + +| Phase | Deliverables | Priority | +|-------|-------------|----------| +| 1.1 | Runtime Core unit tests | ✅ Complete | +| 1.2 | Observability Runtime unit tests | HIGH | +| 1.3 | Observability Hosting unit tests | MEDIUM | +| 1.4 | Observability Extensions tests | MEDIUM | +| 1.5 | Tooling Core unit tests | HIGH | +| 1.6 | Tooling Extensions tests | LOW | +| 1.7 | Notifications unit tests | HIGH | +| 2 | Integration tests | MEDIUM | +| 3 | CI/CD automation | HIGH | + +--- + +## Phase 1: Unit Tests + +### 1.1 Runtime Core Module +**Priority:** HIGH + +| Module | Test File | Status | +|--------|-----------|--------| +| `Utility.cs` | `UtilityTests.cs` | ✅ Complete | +| `AgenticAuthenticationService.cs` | `AgenticAuthenticationServiceTests.cs` | ✅ Complete | +| `TenantContextHelper.cs` | `TenantContextHelperTests.cs` | ✅ Complete | + +--- + +### 1.2 Observability Runtime Module +**Priority:** HIGH + +| Module | Test File | Status | +|--------|-----------|--------| +| `Agent365Exporter.cs` | `Agent365ExporterTests.cs` | ⏳ Partial | +| `Agent365ExporterCore.cs` | `Agent365ExporterCoreTests.cs` | ❌ Missing | +| `AgenticTokenCache.cs` | `AgenticTokenCacheTests.cs` | ❌ Missing | +| `ExportFormatter.cs` | `ExportFormatterTests.cs` | ❌ Missing | +| `OpenTelemetryConstants.cs` | `OpenTelemetryConstantsTests.cs` | ❌ Missing | + +--- + +### 1.3 Observability Hosting Module +**Priority:** MEDIUM + +| Module | Test File | Status | +|--------|-----------|--------| +| Service registration | `ServiceRegistrationTests.cs` | ❌ Missing | +| Configuration validation | `ConfigurationTests.cs` | ❌ Missing | +| Middleware integration | `MiddlewareTests.cs` | ❌ Missing | + +--- + +### 1.4 Observability Extensions +**Priority:** MEDIUM + +| Extension | Status | +|-----------|--------| +| `OpenAI` | ❌ Missing | +| `AgentFramework` | ❌ Missing | +| `SemanticKernel` | ❌ Missing | + +--- + +### 1.5 Tooling Core Module +**Priority:** HIGH + +| Module | Test File | Status | +|--------|-----------|--------| +| `Utility.cs` | `UtilityTests.cs` | ❌ Missing | +| `McpServerConfig.cs` | `McpServerConfigTests.cs` | ❌ Missing | +| `McpToolServerConfigurationService.cs` | `McpToolServerConfigurationServiceTests.cs` | ❌ Missing | + +--- + +### 1.6 Tooling Extensions +**Priority:** LOW + +| Extension | Status | +|-----------|--------| +| `AgentFramework` | ❌ Missing | +| `AzureAIFoundry` | ❌ Missing | +| `SemanticKernel` | ❌ Missing | + +--- + +### 1.7 Notifications Module +**Priority:** HIGH + +| Module | Test File | Status | +|--------|-----------|--------| +| `AgentLifecycleEvent.cs` | `AgentLifecycleEventTests.cs` | ❌ Missing | +| `AgentNotificationActivity.cs` | `AgentNotificationActivityTests.cs` | ❌ Missing | +| `EmailReference.cs` | `EmailReferenceTests.cs` | ❌ Missing | +| `AgentNotification.cs` | `AgentNotificationTests.cs` | ❌ Missing | + +--- + +## Phase 2: Integration Tests + +**Priority:** MEDIUM + +| Integration | Status | +|-------------|--------| +| Runtime + Observability | ❌ Missing | +| Tooling + Runtime | ❌ Missing | +| Notifications + Runtime | ❌ Missing | +| OpenAI end-to-end tracing | ❌ Missing | +| SemanticKernel end-to-end | ❌ Missing | + +--- + +## Phase 3: CI/CD Integration + +**Priority:** HIGH + +| Component | Status | +|-----------|--------| +| GitHub Actions workflow | ⏳ Partial | +| .NET matrix (8.0) | ✅ Complete | +| Coverage enforcement (80%+) | ❌ Missing | +| Codecov integration | ❌ Missing | +| PR blocking on failures | ⏳ Partial | + +--- + +## Success Criteria + +- ✅ 80%+ code coverage for all modules +- ✅ All tests pass independently +- ✅ Full suite completes in < 30 seconds (unit) / < 5 minutes (full) +- ✅ Automated test execution on all PRs +- ✅ Coverage reports visible and enforced