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