Merge branch 'main' into feature-foundry-agents

This commit is contained in:
Roger Barreto
2025-11-14 12:21:57 +00:00
committed by GitHub
111 changed files with 6469 additions and 110 deletions
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
</PropertyGroup>
<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,585 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
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 List<DlpActionInfo>
{
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_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 List<PolicyLocation>
{
new("microsoft.graph.policyLocationApplication", "app-123")
}
};
var expectedResponse = new ProtectionScopesResponse
{
Scopes = new List<PolicyScopeBase>
{
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations = new List<PolicyLocation>
{
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 = new List<PolicyScopeBase>() };
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 - note the endpoint is different from ProcessContent
Assert.Equal("https://graph.microsoft.com/v1.0/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");
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(
new List<ProcessContentMetadataBase> { 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; }
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);
}
// Throw HttpRequestException if configured
if (this.ShouldThrowHttpRequestException)
{
throw new HttpRequestException("Simulated network error");
}
var response = new HttpResponseMessage(this.StatusCodeToReturn);
response.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,571 @@
// 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;
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 IChannelHandler _channelHandler;
private readonly PurviewSettings _settings;
private readonly PurviewWrapper _wrapper;
public PurviewWrapperTests()
{
this._mockProcessor = new Mock<IScopedContentProcessor>();
this._channelHandler = Mock.Of<IChannelHandler>();
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._wrapper = new PurviewWrapper(this._mockProcessor.Object, this._settings, NullLogger.Instance, this._channelHandler);
}
#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>(),
It.IsAny<Activity>(),
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);
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>()))
.ReturnsAsync((false, "user-123")) // Prompt allowed
.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._channelHandler);
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
this._mockProcessor.Verify(x => x.ProcessMessagesAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
"conversation-123",
It.IsAny<Activity>(),
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()), Times.Exactly(2));
}
#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>(),
It.IsAny<Activity>(),
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.Verify(x => x.RunAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<AgentThread>(),
It.IsAny<AgentRunOptions>(),
It.IsAny<CancellationToken>()), Times.Never);
}
[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 AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Sensitive response"));
mockAgent.Setup(x => x.RunAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<AgentThread>(),
It.IsAny<AgentRunOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(innerResponse);
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>()))
.ReturnsAsync((false, "user-123")) // Prompt allowed
.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 AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Safe response"));
mockAgent.Setup(x => x.RunAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<AgentThread>(),
It.IsAny<AgentRunOptions>(),
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.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._channelHandler);
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var expectedResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Response from inner agent"));
var mockAgent = new Mock<AIAgent>();
mockAgent.Setup(x => x.RunAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<AgentThread>(),
It.IsAny<AgentRunOptions>(),
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"))
.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 AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Response"));
var mockAgent = new Mock<AIAgent>();
mockAgent.Setup(x => x.RunAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<AgentThread>(),
It.IsAny<AgentRunOptions>(),
It.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",
It.IsAny<Activity>(),
It.IsAny<PurviewSettings>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()), Times.Exactly(2));
}
[Fact]
public async Task ProcessAgentContentAsync_GeneratesThreadId_WhenNotProvidedAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var expectedResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Response"));
var mockAgent = new Mock<AIAgent>();
mockAgent.Setup(x => x.RunAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<AgentThread>(),
It.IsAny<AgentRunOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedResponse);
string? capturedThreadId = 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, _, _, _, _) => capturedThreadId = 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(capturedThreadId);
Assert.True(Guid.TryParse(capturedThreadId, out _), "Generated thread 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 AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Response"));
mockAgent.Setup(x => x.RunAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<AgentThread>(),
It.IsAny<AgentRunOptions>(),
It.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();
}
}
@@ -0,0 +1,501 @@
// 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.Agents.AI.Purview.Models.Jobs;
using Microsoft.Agents.AI.Purview.Models.Requests;
using Microsoft.Agents.AI.Purview.Models.Responses;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.Purview.UnitTests;
/// <summary>
/// Unit tests for the <see cref="ScopedContentProcessor"/> class.
/// </summary>
public sealed class ScopedContentProcessorTests
{
private readonly Mock<IPurviewClient> _mockPurviewClient;
private readonly Mock<ICacheProvider> _mockCacheProvider;
private readonly Mock<IChannelHandler> _mockChannelHandler;
private readonly ScopedContentProcessor _processor;
public ScopedContentProcessorTests()
{
this._mockPurviewClient = new Mock<IPurviewClient>();
this._mockCacheProvider = new Mock<ICacheProvider>();
this._mockChannelHandler = new Mock<IChannelHandler>();
this._processor = new ScopedContentProcessor(
this._mockPurviewClient.Object,
this._mockCacheProvider.Object,
this._mockChannelHandler.Object);
}
#region ProcessMessagesAsync Tests
[Fact]
public async Task ProcessMessagesAsync_WithBlockAccessAction_ReturnsShouldBlockTrueAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new (ChatRole.User, "Test message")
};
var settings = CreateValidPurviewSettings();
var tokenInfo = new TokenInfo { TenantId = "tenant-123", UserId = "user-123", ClientId = "client-123" };
this._mockPurviewClient.Setup(x => x.GetUserInfoFromTokenAsync(It.IsAny<CancellationToken>(), null))
.ReturnsAsync(tokenInfo);
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((ProtectionScopesResponse?)null);
var psResponse = new ProtectionScopesResponse
{
Scopes = new List<PolicyScopeBase>
{
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations = new List<PolicyLocation>
{
new ("microsoft.graph.policyLocationApplication", "app-123")
},
ExecutionMode = ExecutionMode.EvaluateInline
}
}
};
this._mockPurviewClient.Setup(x => x.GetProtectionScopesAsync(
It.IsAny<ProtectionScopesRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
var pcResponse = new ProcessContentResponse
{
PolicyActions = new List<DlpActionInfo>
{
new() { Action = DlpAction.BlockAccess }
}
};
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
It.IsAny<ProcessContentRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(pcResponse);
// Act
var result = await this._processor.ProcessMessagesAsync(
messages, "thread-123", Activity.UploadText, settings, "user-123", CancellationToken.None);
// Assert
Assert.True(result.shouldBlock);
Assert.Equal("user-123", result.userId);
}
[Fact]
public async Task ProcessMessagesAsync_WithRestrictionActionBlock_ReturnsShouldBlockTrueAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new (ChatRole.User, "Test message")
};
var settings = CreateValidPurviewSettings();
var tokenInfo = new TokenInfo { TenantId = "tenant-123", UserId = "user-123", ClientId = "client-123" };
this._mockPurviewClient.Setup(x => x.GetUserInfoFromTokenAsync(It.IsAny<CancellationToken>(), null))
.ReturnsAsync(tokenInfo);
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((ProtectionScopesResponse?)null);
var psResponse = new ProtectionScopesResponse
{
Scopes = new List<PolicyScopeBase>
{
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations = new List<PolicyLocation>
{
new ("microsoft.graph.policyLocationApplication", "app-123")
},
ExecutionMode = ExecutionMode.EvaluateInline
}
}
};
this._mockPurviewClient.Setup(x => x.GetProtectionScopesAsync(
It.IsAny<ProtectionScopesRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
var pcResponse = new ProcessContentResponse
{
PolicyActions = new List<DlpActionInfo>
{
new() { RestrictionAction = RestrictionAction.Block }
}
};
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
It.IsAny<ProcessContentRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(pcResponse);
// Act
var result = await this._processor.ProcessMessagesAsync(
messages, "thread-123", Activity.UploadText, settings, "user-123", CancellationToken.None);
// Assert
Assert.True(result.shouldBlock);
Assert.Equal("user-123", result.userId);
}
[Fact]
public async Task ProcessMessagesAsync_WithNoBlockingActions_ReturnsShouldBlockFalseAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new (ChatRole.User, "Test message")
};
var settings = CreateValidPurviewSettings();
var tokenInfo = new TokenInfo { TenantId = "tenant-123", UserId = "user-123", ClientId = "client-123" };
this._mockPurviewClient.Setup(x => x.GetUserInfoFromTokenAsync(It.IsAny<CancellationToken>(), null))
.ReturnsAsync(tokenInfo);
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((ProtectionScopesResponse?)null);
var psResponse = new ProtectionScopesResponse
{
Scopes = new List<PolicyScopeBase>
{
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations = new List<PolicyLocation>
{
new("microsoft.graph.policyLocationApplication", "app-123")
},
ExecutionMode = ExecutionMode.EvaluateInline
}
}
};
this._mockPurviewClient.Setup(x => x.GetProtectionScopesAsync(
It.IsAny<ProtectionScopesRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
var pcResponse = new ProcessContentResponse
{
PolicyActions = new List<DlpActionInfo>
{
new() { Action = DlpAction.NotifyUser }
}
};
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
It.IsAny<ProcessContentRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(pcResponse);
// Act
var result = await this._processor.ProcessMessagesAsync(
messages, "thread-123", Activity.UploadText, settings, "user-123", CancellationToken.None);
// Assert
Assert.False(result.shouldBlock);
Assert.Equal("user-123", result.userId);
}
[Fact]
public async Task ProcessMessagesAsync_UsesCachedProtectionScopes_WhenAvailableAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new (ChatRole.User, "Test message")
};
var settings = CreateValidPurviewSettings();
var tokenInfo = new TokenInfo { TenantId = "tenant-123", UserId = "user-123", ClientId = "client-123" };
this._mockPurviewClient.Setup(x => x.GetUserInfoFromTokenAsync(It.IsAny<CancellationToken>(), null))
.ReturnsAsync(tokenInfo);
var cachedPsResponse = new ProtectionScopesResponse
{
Scopes = new List<PolicyScopeBase>
{
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations = new List<PolicyLocation>
{
new ("microsoft.graph.policyLocationApplication", "app-123")
},
ExecutionMode = ExecutionMode.EvaluateInline
}
}
};
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(cachedPsResponse);
var pcResponse = new ProcessContentResponse
{
PolicyActions = new List<DlpActionInfo>()
};
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
It.IsAny<ProcessContentRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(pcResponse);
// Act
await this._processor.ProcessMessagesAsync(
messages, "thread-123", Activity.UploadText, settings, "user-123", CancellationToken.None);
// Assert
this._mockPurviewClient.Verify(x => x.GetProtectionScopesAsync(
It.IsAny<ProtectionScopesRequest>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task ProcessMessagesAsync_InvalidatesCache_WhenProtectionScopeModifiedAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new (ChatRole.User, "Test message")
};
var settings = CreateValidPurviewSettings();
var tokenInfo = new TokenInfo { TenantId = "tenant-123", UserId = "user-123", ClientId = "client-123" };
this._mockPurviewClient.Setup(x => x.GetUserInfoFromTokenAsync(It.IsAny<CancellationToken>(), null))
.ReturnsAsync(tokenInfo);
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((ProtectionScopesResponse?)null);
var psResponse = new ProtectionScopesResponse
{
Scopes = new List<PolicyScopeBase>
{
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations = new List<PolicyLocation>
{
new ("microsoft.graph.policyLocationApplication", "app-123")
},
ExecutionMode = ExecutionMode.EvaluateInline
}
}
};
this._mockPurviewClient.Setup(x => x.GetProtectionScopesAsync(
It.IsAny<ProtectionScopesRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
var pcResponse = new ProcessContentResponse
{
ProtectionScopeState = ProtectionScopeState.Modified,
PolicyActions = new List<DlpActionInfo>()
};
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
It.IsAny<ProcessContentRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(pcResponse);
// Act
await this._processor.ProcessMessagesAsync(
messages, "thread-123", Activity.UploadText, settings, "user-123", CancellationToken.None);
// Assert
this._mockCacheProvider.Verify(x => x.RemoveAsync(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task ProcessMessagesAsync_SendsContentActivities_WhenNoApplicableScopesAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new (ChatRole.User, "Test message")
};
var settings = CreateValidPurviewSettings();
var tokenInfo = new TokenInfo { TenantId = "tenant-123", UserId = "user-123", ClientId = "client-123" };
this._mockPurviewClient.Setup(x => x.GetUserInfoFromTokenAsync(It.IsAny<CancellationToken>(), null))
.ReturnsAsync(tokenInfo);
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((ProtectionScopesResponse?)null);
var psResponse = new ProtectionScopesResponse
{
Scopes = new List<PolicyScopeBase>
{
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations = new List<PolicyLocation>
{
new ("microsoft.graph.policyLocationApplication", "app-456")
}
}
}
};
this._mockPurviewClient.Setup(x => x.GetProtectionScopesAsync(
It.IsAny<ProtectionScopesRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
// Act
await this._processor.ProcessMessagesAsync(
messages, "thread-123", Activity.UploadText, settings, "user-123", CancellationToken.None);
// Assert
// Content activities are now queued as background jobs, not called directly
this._mockChannelHandler.Verify(x => x.QueueJob(It.IsAny<ContentActivityJob>()), Times.Once);
this._mockPurviewClient.Verify(x => x.ProcessContentAsync(
It.IsAny<ProcessContentRequest>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task ProcessMessagesAsync_WithNoTenantId_ThrowsPurviewExceptionAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new (ChatRole.User, "Test message")
};
var settings = new PurviewSettings("TestApp"); // No TenantId
var tokenInfo = new TokenInfo { UserId = "user-123", ClientId = "client-123" }; // No TenantId
this._mockPurviewClient.Setup(x => x.GetUserInfoFromTokenAsync(It.IsAny<CancellationToken>(), null))
.ReturnsAsync(tokenInfo);
// Act & Assert
var exception = await Assert.ThrowsAsync<PurviewRequestException>(() =>
this._processor.ProcessMessagesAsync(messages, "thread-123", Activity.UploadText, settings, "user-123", CancellationToken.None));
Assert.Contains("No tenant id provided or inferred", exception.Message);
}
[Fact]
public async Task ProcessMessagesAsync_WithNoUserId_ThrowsPurviewExceptionAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new (ChatRole.User, "Test message")
};
var settings = CreateValidPurviewSettings();
var tokenInfo = new TokenInfo { TenantId = "tenant-123", ClientId = "client-123" }; // No UserId
this._mockPurviewClient.Setup(x => x.GetUserInfoFromTokenAsync(It.IsAny<CancellationToken>(), null))
.ReturnsAsync(tokenInfo);
// Act & Assert
var exception = await Assert.ThrowsAsync<PurviewRequestException>(() =>
this._processor.ProcessMessagesAsync(messages, "thread-123", Activity.UploadText, settings, null, CancellationToken.None));
Assert.Contains("No user id provided or inferred", exception.Message);
}
[Fact]
public async Task ProcessMessagesAsync_ExtractsUserIdFromMessageAdditionalProperties_Async()
{
// Arrange
var messages = new List<ChatMessage>
{
new (ChatRole.User, "Test message")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ "userId", "user-from-props" }
}
}
};
var settings = CreateValidPurviewSettings();
var tokenInfo = new TokenInfo { TenantId = "tenant-123", ClientId = "client-123" };
this._mockPurviewClient.Setup(x => x.GetUserInfoFromTokenAsync(It.IsAny<CancellationToken>(), null))
.ReturnsAsync(tokenInfo);
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((ProtectionScopesResponse?)null);
var psResponse = new ProtectionScopesResponse { Scopes = new List<PolicyScopeBase>() };
this._mockPurviewClient.Setup(x => x.GetProtectionScopesAsync(
It.IsAny<ProtectionScopesRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
// Act
var result = await this._processor.ProcessMessagesAsync(
messages, "thread-123", Activity.UploadText, settings, null, CancellationToken.None);
// Assert
Assert.Equal("user-from-props", result.userId);
}
[Fact]
public async Task ProcessMessagesAsync_ExtractsUserIdFromMessageAuthorName_WhenValidGuidAsync()
{
// Arrange
var userId = Guid.NewGuid().ToString();
var messages = new List<ChatMessage>
{
new (ChatRole.User, "Test message")
{
AuthorName = userId
}
};
var settings = CreateValidPurviewSettings();
var tokenInfo = new TokenInfo { TenantId = "tenant-123", ClientId = "client-123" };
this._mockPurviewClient.Setup(x => x.GetUserInfoFromTokenAsync(It.IsAny<CancellationToken>(), null))
.ReturnsAsync(tokenInfo);
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((ProtectionScopesResponse?)null);
var psResponse = new ProtectionScopesResponse { Scopes = new List<PolicyScopeBase>() };
this._mockPurviewClient.Setup(x => x.GetProtectionScopesAsync(
It.IsAny<ProtectionScopesRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
// Act
var result = await this._processor.ProcessMessagesAsync(
messages, "thread-123", Activity.UploadText, settings, null, CancellationToken.None);
// Assert
Assert.Equal(userId, result.userId);
}
#endregion
#region Helper Methods
private static PurviewSettings CreateValidPurviewSettings()
{
return new PurviewSettings("TestApp")
{
TenantId = "tenant-123",
PurviewAppLocation = new PurviewAppLocation(PurviewLocationType.Application, "app-123")
};
}
#endregion
}