chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Purview\Microsoft.Agents.AI.Purview.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,609 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Microsoft.Agents.AI.Purview.Models.Common;
using Microsoft.Agents.AI.Purview.Models.Requests;
using Microsoft.Agents.AI.Purview.Models.Responses;
using Microsoft.Agents.AI.Purview.Serialization;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.Agents.AI.Purview.UnitTests;
/// <summary>
/// Unit tests for the <see cref="PurviewClient"/> class.
/// </summary>
public sealed class PurviewClientTests : IDisposable
{
private readonly HttpClient _httpClient;
private readonly PurviewClientHttpMessageHandlerStub _handler;
private readonly PurviewClient _client;
private readonly PurviewSettings _settings;
public PurviewClientTests()
{
this._handler = new PurviewClientHttpMessageHandlerStub();
this._httpClient = new HttpClient(this._handler, false);
this._settings = new PurviewSettings("TestApp")
{
GraphBaseUri = new Uri("https://graph.microsoft.com/v1.0/")
};
var tokenCredential = new MockTokenCredential();
this._client = new PurviewClient(tokenCredential, this._settings, this._httpClient, NullLogger.Instance);
}
#region ProcessContentAsync Tests
[Fact]
public async Task ProcessContentAsync_WithValidRequest_ReturnsSuccessResponseAsync()
{
// Arrange
var request = CreateValidProcessContentRequest();
var expectedResponse = new ProcessContentResponse
{
Id = "test-id-123",
ProtectionScopeState = ProtectionScopeState.NotModified,
PolicyActions =
[
new() { Action = DlpAction.NotifyUser }
]
};
this._handler.StatusCodeToReturn = HttpStatusCode.OK;
this._handler.ResponseToReturn = JsonSerializer.Serialize(expectedResponse, PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ProcessContentResponse)));
// Act
var result = await this._client.ProcessContentAsync(request, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedResponse.Id, result.Id);
Assert.Equal(ProtectionScopeState.NotModified, result.ProtectionScopeState);
Assert.Single(result.PolicyActions!);
Assert.Equal(DlpAction.NotifyUser, result.PolicyActions![0].Action);
// Verify request
Assert.Equal("https://graph.microsoft.com/v1.0/users/test-user-id/dataSecurityAndGovernance/processContent", this._handler.RequestUri?.ToString());
Assert.Equal(HttpMethod.Post, this._handler.RequestMethod);
Assert.Contains("Bearer ", this._handler.AuthorizationHeader);
}
[Fact]
public async Task ProcessContentAsync_WithAcceptedStatus_ReturnsSuccessResponseAsync()
{
// Arrange
var request = CreateValidProcessContentRequest();
var expectedResponse = new ProcessContentResponse
{
Id = "test-id-456",
ProtectionScopeState = ProtectionScopeState.Modified
};
this._handler.StatusCodeToReturn = HttpStatusCode.Accepted;
this._handler.ResponseToReturn = JsonSerializer.Serialize(expectedResponse, PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ProcessContentResponse)));
// Act
var result = await this._client.ProcessContentAsync(request, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedResponse.Id, result.Id);
Assert.Equal(ProtectionScopeState.Modified, result.ProtectionScopeState);
}
[Fact]
public async Task ProcessContentAsync_WithScopeIdentifier_IncludesIfNoneMatchHeaderAsync()
{
// Arrange
var request = CreateValidProcessContentRequest();
request.ScopeIdentifier = "\"test-scope-123\""; // ETags must be quoted
var expectedResponse = new ProcessContentResponse { Id = "test-id" };
this._handler.StatusCodeToReturn = HttpStatusCode.OK;
this._handler.ResponseToReturn = JsonSerializer.Serialize(expectedResponse, PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ProcessContentResponse)));
// Act
await this._client.ProcessContentAsync(request, CancellationToken.None);
// Assert
Assert.Equal("\"test-scope-123\"", this._handler.IfNoneMatchHeader);
}
[Fact]
public async Task ProcessContentAsync_WithProcessInline_IncludesPreferHeaderAsync()
{
// Arrange
var request = CreateValidProcessContentRequest();
request.ProcessInline = true;
var expectedResponse = new ProcessContentResponse { Id = "test-id" };
this._handler.StatusCodeToReturn = HttpStatusCode.OK;
this._handler.ResponseToReturn = JsonSerializer.Serialize(expectedResponse, PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ProcessContentResponse)));
// Act
await this._client.ProcessContentAsync(request, CancellationToken.None);
// Assert
Assert.Equal("evaluateInline", this._handler.PreferHeader);
}
[Fact]
public async Task ProcessContentAsync_WithRateLimitError_ThrowsPurviewRateLimitExceptionAsync()
{
// Arrange
var request = CreateValidProcessContentRequest();
this._handler.StatusCodeToReturn = (HttpStatusCode)429;
// Act & Assert
await Assert.ThrowsAsync<PurviewRateLimitException>(() =>
this._client.ProcessContentAsync(request, CancellationToken.None));
}
[Fact]
public async Task ProcessContentAsync_WithUnauthorizedError_ThrowsPurviewAuthenticationExceptionAsync()
{
// Arrange
var request = CreateValidProcessContentRequest();
this._handler.StatusCodeToReturn = HttpStatusCode.Unauthorized;
// Act & Assert
await Assert.ThrowsAsync<PurviewAuthenticationException>(() =>
this._client.ProcessContentAsync(request, CancellationToken.None));
}
[Fact]
public async Task ProcessContentAsync_WithForbiddenError_ThrowsPurviewAuthenticationExceptionAsync()
{
// Arrange
var request = CreateValidProcessContentRequest();
this._handler.StatusCodeToReturn = HttpStatusCode.Forbidden;
// Act & Assert
await Assert.ThrowsAsync<PurviewAuthenticationException>(() =>
this._client.ProcessContentAsync(request, CancellationToken.None));
}
[Fact]
public async Task ProcessContentAsync_WithPaymentRequiredError_ThrowsPurviewPaymentRequiredExceptionAsync()
{
// Arrange
var request = CreateValidProcessContentRequest();
this._handler.StatusCodeToReturn = HttpStatusCode.PaymentRequired;
// Act & Assert
await Assert.ThrowsAsync<PurviewPaymentRequiredException>(() =>
this._client.ProcessContentAsync(request, CancellationToken.None));
}
[Fact]
public async Task ProcessContentAsync_WithBadRequestError_ThrowsPurviewRequestExceptionAsync()
{
// Arrange
var request = CreateValidProcessContentRequest();
this._handler.StatusCodeToReturn = HttpStatusCode.BadRequest;
// Act & Assert
await Assert.ThrowsAsync<PurviewRequestException>(() =>
this._client.ProcessContentAsync(request, CancellationToken.None));
}
[Fact]
public async Task ProcessContentAsync_WithInvalidJsonResponse_ThrowsPurviewExceptionAsync()
{
// Arrange
var request = CreateValidProcessContentRequest();
this._handler.StatusCodeToReturn = HttpStatusCode.OK;
this._handler.ResponseToReturn = "invalid json";
// Act & Assert
var exception = await Assert.ThrowsAsync<PurviewRequestException>(() =>
this._client.ProcessContentAsync(request, CancellationToken.None));
Assert.Contains("Failed to deserialize ProcessContent response", exception.Message);
Assert.NotNull(exception.InnerException);
Assert.IsType<JsonException>(exception.InnerException);
}
[Fact]
public async Task ProcessContentAsync_WithHttpRequestException_ThrowsPurviewRequestExceptionAsync()
{
// Arrange
var request = CreateValidProcessContentRequest();
this._handler.ShouldThrowHttpRequestException = true;
// Act & Assert
var exception = await Assert.ThrowsAsync<PurviewRequestException>(() =>
this._client.ProcessContentAsync(request, CancellationToken.None));
Assert.Equal("Http error occurred while processing content.", exception.Message);
Assert.NotNull(exception.InnerException);
Assert.IsType<HttpRequestException>(exception.InnerException);
}
#endregion
#region GetProtectionScopesAsync Tests
[Fact]
public async Task GetProtectionScopesAsync_WithValidRequest_ReturnsSuccessResponseAsync()
{
// Arrange
var request = new ProtectionScopesRequest("test-user-id", "test-tenant-id")
{
Activities = ProtectionScopeActivities.UploadText,
Locations =
[
new("microsoft.graph.policyLocationApplication", "app-123")
]
};
var expectedResponse = new ProtectionScopesResponse
{
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations =
[
new ("microsoft.graph.policyLocationApplication", "app-123")
]
}
]
};
this._handler.StatusCodeToReturn = HttpStatusCode.OK;
this._handler.ResponseToReturn = JsonSerializer.Serialize(expectedResponse, PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ProtectionScopesResponse)));
this._handler.ETagToReturn = "\"scope-etag-123\"";
// Act
var result = await this._client.GetProtectionScopesAsync(request, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.NotNull(result.Scopes);
Assert.Single(result.Scopes);
Assert.Equal("\"scope-etag-123\"", result.ScopeIdentifier); // ETags are stored with quotes
// Verify request
Assert.Equal("https://graph.microsoft.com/v1.0/users/test-user-id/dataSecurityAndGovernance/protectionScopes/compute", this._handler.RequestUri?.ToString());
Assert.Equal(HttpMethod.Post, this._handler.RequestMethod);
}
[Fact]
public async Task GetProtectionScopesAsync_SetsETagFromResponse_Async()
{
// Arrange
var request = new ProtectionScopesRequest("test-user-id", "test-tenant-id");
var expectedResponse = new ProtectionScopesResponse { Scopes = [] };
this._handler.StatusCodeToReturn = HttpStatusCode.OK;
this._handler.ResponseToReturn = JsonSerializer.Serialize(expectedResponse, PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ProtectionScopesResponse)));
this._handler.ETagToReturn = "\"custom-etag-456\"";
// Act
var result = await this._client.GetProtectionScopesAsync(request, CancellationToken.None);
// Assert
Assert.Equal("\"custom-etag-456\"", result.ScopeIdentifier);
}
[Fact]
public async Task GetProtectionScopesAsync_WithRateLimitError_ThrowsPurviewRateLimitExceptionAsync()
{
// Arrange
var request = new ProtectionScopesRequest("test-user-id", "test-tenant-id");
this._handler.StatusCodeToReturn = (HttpStatusCode)429;
// Act & Assert
await Assert.ThrowsAsync<PurviewRateLimitException>(() =>
this._client.GetProtectionScopesAsync(request, CancellationToken.None));
}
[Fact]
public async Task GetProtectionScopesAsync_WithUnauthorizedError_ThrowsPurviewAuthenticationExceptionAsync()
{
// Arrange
var request = new ProtectionScopesRequest("test-user-id", "test-tenant-id");
this._handler.StatusCodeToReturn = HttpStatusCode.Unauthorized;
// Act & Assert
await Assert.ThrowsAsync<PurviewAuthenticationException>(() =>
this._client.GetProtectionScopesAsync(request, CancellationToken.None));
}
[Fact]
public async Task GetProtectionScopesAsync_WithInvalidJsonResponse_ThrowsPurviewExceptionAsync()
{
// Arrange
var request = new ProtectionScopesRequest("test-user-id", "test-tenant-id");
this._handler.StatusCodeToReturn = HttpStatusCode.OK;
this._handler.ResponseToReturn = "invalid json";
// Act & Assert
var exception = await Assert.ThrowsAsync<PurviewRequestException>(() =>
this._client.GetProtectionScopesAsync(request, CancellationToken.None));
Assert.Contains("Failed to deserialize ProtectionScopes response", exception.Message);
Assert.NotNull(exception.InnerException);
Assert.IsType<JsonException>(exception.InnerException);
}
[Fact]
public async Task GetProtectionScopesAsync_WithHttpRequestException_ThrowsPurviewRequestExceptionAsync()
{
// Arrange
var request = new ProtectionScopesRequest("test-user-id", "test-tenant-id");
this._handler.ShouldThrowHttpRequestException = true;
// Act & Assert
var exception = await Assert.ThrowsAsync<PurviewRequestException>(() =>
this._client.GetProtectionScopesAsync(request, CancellationToken.None));
Assert.Equal("Http error occurred while retrieving protection scopes.", exception.Message);
Assert.NotNull(exception.InnerException);
Assert.IsType<HttpRequestException>(exception.InnerException);
}
#endregion
#region SendContentActivitiesAsync Tests
[Fact]
public async Task SendContentActivitiesAsync_WithValidRequest_ReturnsSuccessResponseAsync()
{
// Arrange
var contentToProcess = CreateValidContentToProcess();
var request = new ContentActivitiesRequest("test-user-id", "test-tenant-id", contentToProcess);
var expectedResponse = new ContentActivitiesResponse
{
StatusCode = HttpStatusCode.Created
};
this._handler.StatusCodeToReturn = HttpStatusCode.Created;
this._handler.ResponseToReturn = JsonSerializer.Serialize(expectedResponse, PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ContentActivitiesResponse)));
// Act
var result = await this._client.SendContentActivitiesAsync(request, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Null(result.Error);
// Verify request
Assert.Equal("https://graph.microsoft.com/v1.0/users/test-user-id/dataSecurityAndGovernance/activities/contentActivities", this._handler.RequestUri?.ToString());
Assert.Equal(HttpMethod.Post, this._handler.RequestMethod);
}
[Fact]
public async Task SendContentActivitiesAsync_WithError_ReturnsResponseWithErrorAsync()
{
// Arrange
var contentToProcess = CreateValidContentToProcess();
var request = new ContentActivitiesRequest("test-user-id", "test-tenant-id", contentToProcess);
var expectedResponse = new ContentActivitiesResponse
{
Error = new ErrorDetails
{
Code = "InvalidRequest",
Message = "The request is invalid"
}
};
this._handler.StatusCodeToReturn = HttpStatusCode.Created;
this._handler.ResponseToReturn = JsonSerializer.Serialize(expectedResponse, PurviewSerializationUtils.SerializationSettings.GetTypeInfo(typeof(ContentActivitiesResponse)));
// Act
var result = await this._client.SendContentActivitiesAsync(request, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.NotNull(result.Error);
Assert.Equal("InvalidRequest", result.Error.Code);
Assert.Equal("The request is invalid", result.Error.Message);
}
[Fact]
public async Task SendContentActivitiesAsync_WithRateLimitError_ThrowsPurviewRateLimitExceptionAsync()
{
// Arrange
var contentToProcess = CreateValidContentToProcess();
var request = new ContentActivitiesRequest("test-user-id", "test-tenant-id", contentToProcess);
this._handler.StatusCodeToReturn = (HttpStatusCode)429;
// Act & Assert
await Assert.ThrowsAsync<PurviewRateLimitException>(() =>
this._client.SendContentActivitiesAsync(request, CancellationToken.None));
}
[Fact]
public async Task SendContentActivitiesAsync_WithUnauthorizedError_ThrowsPurviewAuthenticationExceptionAsync()
{
// Arrange
var contentToProcess = CreateValidContentToProcess();
var request = new ContentActivitiesRequest("test-user-id", "test-tenant-id", contentToProcess);
this._handler.StatusCodeToReturn = HttpStatusCode.Unauthorized;
// Act & Assert
await Assert.ThrowsAsync<PurviewAuthenticationException>(() =>
this._client.SendContentActivitiesAsync(request, CancellationToken.None));
}
[Fact]
public async Task SendContentActivitiesAsync_WithBadRequestError_ThrowsPurviewRequestExceptionAsync()
{
// Arrange
var contentToProcess = CreateValidContentToProcess();
var request = new ContentActivitiesRequest("test-user-id", "test-tenant-id", contentToProcess);
this._handler.StatusCodeToReturn = HttpStatusCode.BadRequest;
// Act & Assert
await Assert.ThrowsAsync<PurviewRequestException>(() =>
this._client.SendContentActivitiesAsync(request, CancellationToken.None));
}
[Fact]
public async Task SendContentActivitiesAsync_WithInvalidJsonResponse_ThrowsPurviewExceptionAsync()
{
// Arrange
var contentToProcess = CreateValidContentToProcess();
var request = new ContentActivitiesRequest("test-user-id", "test-tenant-id", contentToProcess);
this._handler.StatusCodeToReturn = HttpStatusCode.Created;
this._handler.ResponseToReturn = "invalid json";
// Act & Assert
var exception = await Assert.ThrowsAsync<PurviewRequestException>(() =>
this._client.SendContentActivitiesAsync(request, CancellationToken.None));
Assert.Contains("Failed to deserialize ContentActivities response", exception.Message);
Assert.NotNull(exception.InnerException);
Assert.IsType<JsonException>(exception.InnerException);
}
[Fact]
public async Task SendContentActivitiesAsync_WithHttpRequestException_ThrowsPurviewRequestExceptionAsync()
{
// Arrange
var contentToProcess = CreateValidContentToProcess();
var request = new ContentActivitiesRequest("test-user-id", "test-tenant-id", contentToProcess);
this._handler.ShouldThrowHttpRequestException = true;
// Act & Assert
var exception = await Assert.ThrowsAsync<PurviewRequestException>(() =>
this._client.SendContentActivitiesAsync(request, CancellationToken.None));
Assert.Equal("Http error occurred while creating content activities.", exception.Message);
Assert.NotNull(exception.InnerException);
Assert.IsType<HttpRequestException>(exception.InnerException);
}
#endregion
#region Helper Methods
private static ProcessContentRequest CreateValidProcessContentRequest()
{
var contentToProcess = CreateValidContentToProcess();
return new ProcessContentRequest(contentToProcess, "test-user-id", "test-tenant-id");
}
private static ContentToProcess CreateValidContentToProcess()
{
var content = new PurviewTextContent("Test content");
var metadata = new ProcessConversationMetadata(content, "msg-123", false, "Test message", "test-correlation-id");
var activityMetadata = new ActivityMetadata(Activity.UploadText);
var deviceMetadata = new DeviceMetadata
{
OperatingSystemSpecifications = new OperatingSystemSpecifications
{
OperatingSystemPlatform = "Windows",
OperatingSystemVersion = "10"
}
};
var integratedAppMetadata = new IntegratedAppMetadata
{
Name = "TestApp",
Version = "1.0"
};
var policyLocation = new PolicyLocation("microsoft.graph.policyLocationApplication", "app-123");
var protectedAppMetadata = new ProtectedAppMetadata(policyLocation)
{
Name = "TestApp",
Version = "1.0"
};
return new ContentToProcess(
[metadata],
activityMetadata,
deviceMetadata,
integratedAppMetadata,
protectedAppMetadata
);
}
#endregion
public void Dispose()
{
this._handler.Dispose();
this._httpClient.Dispose();
}
/// <summary>
/// Mock HTTP message handler for testing
/// </summary>
internal sealed class PurviewClientHttpMessageHandlerStub : HttpMessageHandler
{
public HttpStatusCode StatusCodeToReturn { get; set; } = HttpStatusCode.OK;
public string? ResponseToReturn { get; set; }
public string? ETagToReturn { get; set; }
public bool ShouldThrowHttpRequestException { get; set; }
public Uri? RequestUri { get; private set; }
public HttpMethod? RequestMethod { get; private set; }
public string? AuthorizationHeader { get; private set; }
public string? IfNoneMatchHeader { get; private set; }
public string? PreferHeader { get; private set; }
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Capture request details
this.RequestUri = request.RequestUri;
this.RequestMethod = request.Method;
if (request.Headers.Authorization != null)
{
this.AuthorizationHeader = request.Headers.Authorization.ToString();
}
if (request.Headers.TryGetValues("If-None-Match", out var ifNoneMatchValues))
{
this.IfNoneMatchHeader = string.Join(", ", ifNoneMatchValues);
}
if (request.Headers.TryGetValues("Prefer", out var preferValues))
{
this.PreferHeader = string.Join(", ", preferValues);
}
// Throw HttpRequestException if configured
if (this.ShouldThrowHttpRequestException)
{
throw new HttpRequestException("Simulated network error");
}
var response = new HttpResponseMessage(this.StatusCodeToReturn)
{
Content = new StringContent(this.ResponseToReturn ?? string.Empty, Encoding.UTF8, "application/json")
};
if (!string.IsNullOrEmpty(this.ETagToReturn))
{
response.Headers.ETag = new System.Net.Http.Headers.EntityTagHeaderValue(this.ETagToReturn);
}
return await Task.FromResult(response);
}
}
/// <summary>
/// Mock token credential for testing
/// </summary>
internal sealed class MockTokenCredential : TokenCredential
{
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return new AccessToken("mock-token", DateTimeOffset.UtcNow.AddHours(1));
}
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return new ValueTask<AccessToken>(new AccessToken("mock-token", DateTimeOffset.UtcNow.AddHours(1)));
}
}
}
@@ -0,0 +1,612 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Purview.Models.Common;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Moq.Protected;
namespace Microsoft.Agents.AI.Purview.UnitTests;
/// <summary>
/// Unit tests for the <see cref="PurviewWrapper"/> class.
/// </summary>
public sealed class PurviewWrapperTests : IDisposable
{
private readonly Mock<IScopedContentProcessor> _mockProcessor;
private readonly IBackgroundJobRunner _backgroundJobRunner;
private readonly PurviewSettings _settings;
private readonly PurviewWrapper _wrapper;
public PurviewWrapperTests()
{
this._mockProcessor = new Mock<IScopedContentProcessor>();
this._settings = new PurviewSettings("TestApp")
{
TenantId = "tenant-123",
PurviewAppLocation = new PurviewAppLocation(PurviewLocationType.Application, "app-123"),
BlockedPromptMessage = "Prompt blocked by policy",
BlockedResponseMessage = "Response blocked by policy"
};
this._backgroundJobRunner = Mock.Of<IBackgroundJobRunner>();
this._wrapper = new PurviewWrapper(this._mockProcessor.Object, this._settings, NullLogger.Instance, this._backgroundJobRunner);
}
#region ProcessChatContentAsync Tests
[Fact]
public async Task ProcessChatContentAsync_WithBlockedPrompt_ReturnsBlockedMessageAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Sensitive content that should be blocked")
};
var mockChatClient = new Mock<IChatClient>();
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<string>(),
Activity.UploadText,
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync((true, "user-123"));
// Act
var result = await this._wrapper.ProcessChatContentAsync(messages, null, mockChatClient.Object, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Single(result.Messages);
Assert.Equal(ChatRole.System, result.Messages[0].Role);
Assert.Equal("Prompt blocked by policy", result.Messages[0].Text);
mockChatClient.Verify(x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task ProcessChatContentAsync_WithAllowedPromptAndBlockedResponse_ReturnsBlockedMessageAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var mockChatClient = new Mock<IChatClient>();
var innerResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Sensitive response"));
mockChatClient.Setup(x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(innerResponse);
// Prompt check uses UploadText, response check uses DownloadText
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<string>(),
Activity.UploadText,
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync((false, "user-123")); // Prompt allowed
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<string>(),
Activity.DownloadText,
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync((true, "user-123")); // Response blocked
// Act
var result = await this._wrapper.ProcessChatContentAsync(messages, null, mockChatClient.Object, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Single(result.Messages);
Assert.Equal(ChatRole.System, result.Messages[0].Role);
Assert.Equal("Response blocked by policy", result.Messages[0].Text);
}
[Fact]
public async Task ProcessChatContentAsync_WithAllowedPromptAndResponse_ReturnsInnerResponseAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var mockChatClient = new Mock<IChatClient>();
var innerResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Safe response"));
mockChatClient.Setup(x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(innerResponse);
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<string>(),
It.IsAny<Activity>(),
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync((false, "user-123"));
// Act
var result = await this._wrapper.ProcessChatContentAsync(messages, null, mockChatClient.Object, CancellationToken.None);
// Assert
Assert.Same(innerResponse, result);
}
[Fact]
public async Task ProcessChatContentAsync_WithIgnoreExceptions_ContinuesOnPromptErrorAsync()
{
// Arrange
var settingsWithIgnore = new PurviewSettings("TestApp")
{
TenantId = "tenant-123",
IgnoreExceptions = true,
PurviewAppLocation = new PurviewAppLocation(PurviewLocationType.Application, "app-123")
};
var wrapper = new PurviewWrapper(this._mockProcessor.Object, settingsWithIgnore, NullLogger.Instance, this._backgroundJobRunner);
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var expectedResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Response from inner client"));
var mockChatClient = new Mock<IChatClient>();
mockChatClient.Setup(x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedResponse);
this._mockProcessor.SetupSequence(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<string>(),
It.IsAny<Activity>(),
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ThrowsAsync(new PurviewRequestException("Prompt processing error")); // Response processing succeeds
// Act
var result = await wrapper.ProcessChatContentAsync(messages, null, mockChatClient.Object, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Same(expectedResponse, result);
}
[Fact]
public async Task ProcessChatContentAsync_WithoutIgnoreExceptions_ThrowsOnPromptErrorAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var mockChatClient = new Mock<IChatClient>();
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<string>(),
It.IsAny<Activity>(),
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ThrowsAsync(new PurviewRequestException("Prompt processing error"));
// Act & Assert
await Assert.ThrowsAsync<PurviewRequestException>(() =>
this._wrapper.ProcessChatContentAsync(messages, null, mockChatClient.Object, CancellationToken.None));
}
[Fact]
public async Task ProcessChatContentAsync_UsesConversationIdFromOptions_Async()
{
// Arrange
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var options = new ChatOptions { ConversationId = "conversation-123" };
var mockChatClient = new Mock<IChatClient>();
var innerResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Response"));
mockChatClient.Setup(x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(innerResponse);
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
"conversation-123",
It.IsAny<Activity>(),
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync((false, "user-123"));
// Act
await this._wrapper.ProcessChatContentAsync(messages, options, mockChatClient.Object, CancellationToken.None);
// Assert - verify prompt uses UploadText and response uses DownloadText
this._mockProcessor.Verify(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
"conversation-123",
Activity.UploadText,
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()), Times.Once);
this._mockProcessor.Verify(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
"conversation-123",
Activity.DownloadText,
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()), Times.Once);
}
#endregion
#region ProcessAgentContentAsync Tests
[Fact]
public async Task ProcessAgentContentAsync_WithBlockedPrompt_ReturnsBlockedMessageAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Sensitive content")
};
var mockAgent = new Mock<AIAgent>();
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<string>(),
Activity.UploadText,
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync((true, "user-123"));
// Act
var result = await this._wrapper.ProcessAgentContentAsync(messages, null, null, mockAgent.Object, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Single(result.Messages);
Assert.Equal(ChatRole.System, result.Messages[0].Role);
Assert.Equal("Prompt blocked by policy", result.Messages[0].Text);
mockAgent.Protected().Verify("RunCoreAsync",
Times.Never(),
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession>(),
ItExpr.IsAny<AgentRunOptions>(),
ItExpr.IsAny<CancellationToken>());
}
[Fact]
public async Task ProcessAgentContentAsync_WithAllowedPromptAndBlockedResponse_ReturnsBlockedMessageAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var mockAgent = new Mock<AIAgent>();
var innerResponse = new AgentResponse(new ChatMessage(ChatRole.Assistant, "Sensitive response"));
mockAgent.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession>(),
ItExpr.IsAny<AgentRunOptions>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(innerResponse);
// Prompt check uses UploadText, response check uses DownloadText
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<string>(),
Activity.UploadText,
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync((false, "user-123")); // Prompt allowed
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<string>(),
Activity.DownloadText,
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync((true, "user-123")); // Response blocked
// Act
var result = await this._wrapper.ProcessAgentContentAsync(messages, null, null, mockAgent.Object, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Single(result.Messages);
Assert.Equal(ChatRole.System, result.Messages[0].Role);
Assert.Equal("Response blocked by policy", result.Messages[0].Text);
}
[Fact]
public async Task ProcessAgentContentAsync_WithAllowedPromptAndResponse_ReturnsInnerResponseAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var mockAgent = new Mock<AIAgent>();
var innerResponse = new AgentResponse(new ChatMessage(ChatRole.Assistant, "Safe response"));
mockAgent.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession>(),
ItExpr.IsAny<AgentRunOptions>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(innerResponse);
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<string>(),
It.IsAny<Activity>(),
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync((false, "user-123"));
// Act
var result = await this._wrapper.ProcessAgentContentAsync(messages, null, null, mockAgent.Object, CancellationToken.None);
// Assert
Assert.Same(innerResponse, result);
}
[Fact]
public async Task ProcessAgentContentAsync_WithIgnoreExceptions_ContinuesOnErrorAsync()
{
// Arrange
var settingsWithIgnore = new PurviewSettings("TestApp")
{
TenantId = "tenant-123",
IgnoreExceptions = true,
PurviewAppLocation = new PurviewAppLocation(PurviewLocationType.Application, "app-123")
};
var wrapper = new PurviewWrapper(this._mockProcessor.Object, settingsWithIgnore, NullLogger.Instance, this._backgroundJobRunner);
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var expectedResponse = new AgentResponse(new ChatMessage(ChatRole.Assistant, "Response from inner agent"));
var mockAgent = new Mock<AIAgent>();
mockAgent.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession>(),
ItExpr.IsAny<AgentRunOptions>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(expectedResponse);
this._mockProcessor.SetupSequence(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<string>(),
It.IsAny<Activity>(),
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ThrowsAsync(new PurviewRequestException("Prompt processing error"))
.ReturnsAsync((false, "user-123")); // Response processing succeeds
// Act
var result = await wrapper.ProcessAgentContentAsync(messages, null, null, mockAgent.Object, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Same(expectedResponse, result);
}
[Fact]
public async Task ProcessAgentContentAsync_WithoutIgnoreExceptions_ThrowsOnErrorAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var mockAgent = new Mock<AIAgent>();
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<string>(),
It.IsAny<Activity>(),
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ThrowsAsync(new PurviewRequestException("Processing error"));
// Act & Assert
await Assert.ThrowsAsync<PurviewRequestException>(() =>
this._wrapper.ProcessAgentContentAsync(messages, null, null, mockAgent.Object, CancellationToken.None));
}
[Fact]
public async Task ProcessAgentContentAsync_ExtractsThreadIdFromMessageAdditionalProperties_Async()
{
// Arrange
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ "conversationId", "conversation-from-props" }
}
}
};
var expectedResponse = new AgentResponse(new ChatMessage(ChatRole.Assistant, "Response"));
var mockAgent = new Mock<AIAgent>();
mockAgent.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession>(),
ItExpr.IsAny<AgentRunOptions>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(expectedResponse);
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
"conversation-from-props",
It.IsAny<Activity>(),
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync((false, "user-123"));
// Act
var result = await this._wrapper.ProcessAgentContentAsync(messages, null, null, mockAgent.Object, CancellationToken.None);
// Assert
Assert.NotNull(result);
this._mockProcessor.Verify(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
"conversation-from-props",
Activity.UploadText,
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()), Times.Once);
this._mockProcessor.Verify(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
"conversation-from-props",
Activity.DownloadText,
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task ProcessAgentContentAsync_GeneratesThreadId_WhenNotProvidedAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var expectedResponse = new AgentResponse(new ChatMessage(ChatRole.Assistant, "Response"));
var mockAgent = new Mock<AIAgent>();
mockAgent.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession>(),
ItExpr.IsAny<AgentRunOptions>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(expectedResponse);
string? capturedSessionId = null;
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<string>(),
It.IsAny<Activity>(),
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, string, Activity, PurviewSettings, string, CancellationToken>(
(_, threadId, _, _, _, _) => capturedSessionId = threadId)
.ReturnsAsync((false, "user-123"));
// Act
var result = await this._wrapper.ProcessAgentContentAsync(messages, null, null, mockAgent.Object, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.NotNull(capturedSessionId);
Assert.True(Guid.TryParse(capturedSessionId, out _), "Generated session ID should be a valid GUID");
}
[Fact]
public async Task ProcessAgentContentAsync_PassesResolvedUserId_ToResponseProcessingAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var mockAgent = new Mock<AIAgent>();
var innerResponse = new AgentResponse(new ChatMessage(ChatRole.Assistant, "Response"));
mockAgent.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession>(),
ItExpr.IsAny<AgentRunOptions>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(innerResponse);
var callCount = 0;
string? firstCallUserId = null;
string? secondCallUserId = null;
this._mockProcessor.Setup(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<string>(),
It.IsAny<Activity>(),
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, string, Activity, PurviewSettings, string, CancellationToken>(
(_, _, _, _, userId, _) =>
{
if (callCount == 0)
{
firstCallUserId = userId;
}
else if (callCount == 1)
{
secondCallUserId = userId;
}
callCount++;
})
.ReturnsAsync((false, "resolved-user-456"));
// Act
await this._wrapper.ProcessAgentContentAsync(messages, null, null, mockAgent.Object, CancellationToken.None);
// Assert
Assert.Null(firstCallUserId); // First call (prompt) should have null userId
Assert.Equal("resolved-user-456", secondCallUserId); // Second call (response) should have resolved userId from first call
}
#endregion
public void Dispose()
{
this._wrapper.Dispose();
}
}