Files
agent-framework/dotnet/tests/Microsoft.Agents.AI.Purview.UnitTests/ScopedContentProcessorTests.cs
Taisir Hassan 383d551b86 Purview: Parallelize PSPC cold-cache scope refresh (#5832)
* Parallelize Purview PSPC cold cache path

* Cache Purview payment-required state for scope refresh

* Cache Purview payment-required state for scope refresh

* Align Purview policy action dedupe and 402 caching

 Deduplicate combined policy actions by action and restriction action so restriction-only actions are preserved
without duplicating identical entries. Cache tenant-level payment-required state from background scope refresh so
subsequent calls short-circuit consistently.

* .NET: Implement best-effort caching for background job scope retrieval and add unit tests for cache write failures

* Purview - feat: Enhance ScopedContentProcessor to queue ContentActivityJob when no applicable scopes are found and update related tests

* docs: Update purview package README and AGENTS documentation to reflect caching optimizations and policy enforcement scenarios

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-09 18:01:21 +00:00

820 lines
32 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Channels;
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 Microsoft.Extensions.Logging.Abstractions;
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);
var psResponse = new ProtectionScopesResponse
{
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations =
[
new ("microsoft.graph.policyLocationApplication", "app-123")
],
ExecutionMode = ExecutionMode.EvaluateInline
}
]
};
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
var pcResponse = new ProcessContentResponse
{
PolicyActions =
[
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, "session-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);
var psResponse = new ProtectionScopesResponse
{
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations =
[
new ("microsoft.graph.policyLocationApplication", "app-123")
],
ExecutionMode = ExecutionMode.EvaluateInline
}
]
};
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
var pcResponse = new ProcessContentResponse
{
PolicyActions =
[
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, "session-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);
var psResponse = new ProtectionScopesResponse
{
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations =
[
new("microsoft.graph.policyLocationApplication", "app-123")
],
ExecutionMode = ExecutionMode.EvaluateInline
}
]
};
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
var pcResponse = new ProcessContentResponse
{
PolicyActions =
[
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, "session-123", Activity.UploadText, settings, "user-123", CancellationToken.None);
// Assert
Assert.False(result.shouldBlock);
Assert.Equal("user-123", result.userId);
}
[Fact]
public async Task ProcessMessagesAsync_DeduplicatesCombinedPolicyActionsByActionAndRestrictionAsync()
{
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Test message")
];
PurviewSettings settings = CreateValidPurviewSettings();
TokenInfo tokenInfo = new() { TenantId = "tenant-123", UserId = "user-123", ClientId = "client-123" };
DlpActionInfo processContentAction = new() { Action = DlpAction.BlockAccess, RestrictionAction = RestrictionAction.Block };
DlpActionInfo duplicateScopeAction = new() { Action = DlpAction.BlockAccess, RestrictionAction = RestrictionAction.Block };
DlpActionInfo restrictionOnlyAction = new() { RestrictionAction = RestrictionAction.Block };
ProcessContentResponse pcResponse = new()
{
PolicyActions =
[
processContentAction
]
};
ProtectionScopesResponse psResponse = new()
{
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations =
[
new("microsoft.graph.policyLocationApplication", "app-123")
],
ExecutionMode = ExecutionMode.EvaluateInline,
PolicyActions =
[
duplicateScopeAction,
restrictionOnlyAction
]
}
]
};
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(psResponse);
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
It.IsAny<ProcessContentRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(pcResponse);
// Act
await this._processor.ProcessMessagesAsync(
messages, "session-123", Activity.UploadText, settings, "user-123", CancellationToken.None);
// Assert
Assert.NotNull(pcResponse.PolicyActions);
Assert.Equal(2, pcResponse.PolicyActions.Count);
Assert.Same(processContentAction, pcResponse.PolicyActions[0]);
Assert.Same(restrictionOnlyAction, pcResponse.PolicyActions[1]);
}
[Fact]
public void CheckApplicableScopes_MatchesAnyLocationInScope()
{
// Arrange
ProcessContentRequest pcRequest = CreateProcessContentRequest();
ProtectionScopesResponse psResponse = new()
{
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations =
[
new("microsoft.graph.policyLocationApplication", "app-123"),
new("microsoft.graph.policyLocationApplication", "different-app")
],
ExecutionMode = ExecutionMode.EvaluateInline
}
]
};
// Act
(bool shouldProcess, _, ExecutionMode executionMode) = ScopedContentProcessor.CheckApplicableScopes(pcRequest, psResponse);
// Assert
Assert.True(shouldProcess);
Assert.Equal(ExecutionMode.EvaluateInline, executionMode);
}
[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()
{
Activities = ProtectionScopeActivities.UploadText,
Locations =
[
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 = []
};
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
It.IsAny<ProcessContentRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(pcResponse);
// Act
await this._processor.ProcessMessagesAsync(
messages, "session-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);
var psResponse = new ProtectionScopesResponse
{
ScopeIdentifier = "etag-1",
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations =
[
new ("microsoft.graph.policyLocationApplication", "app-123")
],
ExecutionMode = ExecutionMode.EvaluateInline
}
]
};
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
var pcResponse = new ProcessContentResponse
{
ProtectionScopeState = ProtectionScopeState.Modified,
PolicyActions = []
};
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
It.IsAny<ProcessContentRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(pcResponse);
// Act
await this._processor.ProcessMessagesAsync(
messages, "session-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);
var psResponse = new ProtectionScopesResponse
{
Scopes =
[
new()
{
Activities = ProtectionScopeActivities.UploadText,
Locations =
[
new ("microsoft.graph.policyLocationApplication", "app-456")
]
}
]
};
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
// Act
await this._processor.ProcessMessagesAsync(
messages, "session-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, "session-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, "session-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);
var psResponse = new ProtectionScopesResponse { Scopes = [] };
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
// Act
var result = await this._processor.ProcessMessagesAsync(
messages, "session-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);
var psResponse = new ProtectionScopesResponse { Scopes = [] };
this._mockCacheProvider.Setup(x => x.GetAsync<ProtectionScopesCacheKey, ProtectionScopesResponse>(
It.IsAny<ProtectionScopesCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(psResponse);
// Act
var result = await this._processor.ProcessMessagesAsync(
messages, "session-123", Activity.UploadText, settings, null, CancellationToken.None);
// Assert
Assert.Equal(userId, result.userId);
}
[Fact]
public async Task ProcessMessagesAsync_CacheMiss_QueuesScopeRetrievalJobAndCallsProcessContentAsync()
{
// 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);
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
It.IsAny<ProcessContentRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ProcessContentResponse());
// Act
await this._processor.ProcessMessagesAsync(
messages, "session-123", Activity.UploadText, settings, "user-123", CancellationToken.None);
// Assert: ProcessContent runs in the foreground; GetProtectionScopes is queued as a background job.
this._mockPurviewClient.Verify(x => x.ProcessContentAsync(
It.IsAny<ProcessContentRequest>(), It.IsAny<CancellationToken>()), Times.Once);
this._mockPurviewClient.Verify(x => x.GetProtectionScopesAsync(
It.IsAny<ProtectionScopesRequest>(), It.IsAny<CancellationToken>()), Times.Never);
this._mockChannelHandler.Verify(x => x.QueueJob(It.IsAny<ScopeRetrievalJob>()), Times.Once);
}
[Fact]
public async Task ProcessMessagesAsync_CacheMiss_WithProcessContentBlockAction_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 pcResponse = new ProcessContentResponse
{
PolicyActions =
[
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, "session-123", Activity.UploadText, settings, "user-123", CancellationToken.None);
// Assert
Assert.True(result.shouldBlock);
this._mockChannelHandler.Verify(x => x.QueueJob(It.IsAny<ScopeRetrievalJob>()), Times.Once);
}
[Fact]
public async Task ProcessMessagesAsync_CacheMiss_StillCallsProcessContentWhenScopeJobCannotQueueAsync()
{
// 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);
this._mockChannelHandler.Setup(x => x.QueueJob(It.IsAny<ScopeRetrievalJob>()))
.Throws(new PurviewJobException("queue unavailable"));
this._mockPurviewClient.Setup(x => x.ProcessContentAsync(
It.IsAny<ProcessContentRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ProcessContentResponse());
// Act
await this._processor.ProcessMessagesAsync(
messages, "session-123", Activity.UploadText, settings, "user-123", CancellationToken.None);
// Assert: scope warmup is attempted, and ProcessContent still runs when it can't be queued.
this._mockChannelHandler.Verify(x => x.QueueJob(It.IsAny<ScopeRetrievalJob>()), Times.Once);
this._mockPurviewClient.Verify(x => x.ProcessContentAsync(
It.IsAny<ProcessContentRequest>(), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task ProcessMessagesAsync_WithCachedPaymentRequiredState_ThrowsPaymentRequiredAsync()
{
// 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<PaymentRequiredCacheKey, PaymentRequiredCacheEntry>(
It.IsAny<PaymentRequiredCacheKey>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PaymentRequiredCacheEntry("Payment required"));
// Act + Assert
await Assert.ThrowsAsync<PurviewPaymentRequiredException>(() =>
this._processor.ProcessMessagesAsync(
messages, "session-123", Activity.UploadText, settings, "user-123", CancellationToken.None));
this._mockPurviewClient.Verify(x => x.ProcessContentAsync(
It.IsAny<ProcessContentRequest>(), It.IsAny<CancellationToken>()), Times.Never);
this._mockChannelHandler.Verify(x => x.QueueJob(It.IsAny<ScopeRetrievalJob>()), Times.Never);
}
[Fact]
public async Task BackgroundJobRunner_ScopeRetrievalPaymentRequired_CachesForSubsequentCallsAsync()
{
// Arrange
Func<Channel<BackgroundJobBase>, Task>? runner = null;
Mock<IChannelHandler> channelHandler = new();
Mock<IPurviewClient> purviewClient = new();
Mock<ICacheProvider> cacheProvider = new();
PurviewSettings settings = new("TestApp") { MaxConcurrentJobConsumers = 1 };
ProtectionScopesRequest request = new("user-123", "tenant-123")
{
Activities = ProtectionScopeActivities.UploadText,
Locations =
[
new("microsoft.graph.policyLocationApplication", "app-123")
]
};
ProtectionScopesCacheKey cacheKey = new(request);
Channel<BackgroundJobBase> channel = Channel.CreateUnbounded<BackgroundJobBase>();
channelHandler.Setup(x => x.AddRunner(It.IsAny<Func<Channel<BackgroundJobBase>, Task>>()))
.Callback<Func<Channel<BackgroundJobBase>, Task>>(callback => runner = callback);
purviewClient.Setup(x => x.GetProtectionScopesAsync(It.IsAny<ProtectionScopesRequest>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new PurviewPaymentRequiredException("Payment required"));
_ = new BackgroundJobRunner(channelHandler.Object, purviewClient.Object, cacheProvider.Object, NullLogger.Instance, settings);
// Act
Assert.NotNull(runner);
await channel.Writer.WriteAsync(new ScopeRetrievalJob(request, cacheKey, CreateProcessContentRequest()));
channel.Writer.Complete();
await runner(channel);
// Assert
cacheProvider.Verify(x => x.SetAsync(
It.Is<PaymentRequiredCacheKey>(key => key.TenantId == "tenant-123"),
It.Is<PaymentRequiredCacheEntry>(entry => entry.Message == "Payment required"),
It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task BackgroundJobRunner_ScopeRetrievalNoApplicableScopes_QueuesContentActivityJobAsync()
{
// Arrange
Func<Channel<BackgroundJobBase>, Task>? runner = null;
Mock<IChannelHandler> channelHandler = new();
Mock<IPurviewClient> purviewClient = new();
Mock<ICacheProvider> cacheProvider = new();
PurviewSettings settings = new("TestApp") { MaxConcurrentJobConsumers = 1 };
ProtectionScopesRequest request = CreateProtectionScopesRequest();
ScopeRetrievalJob job = new(request, new ProtectionScopesCacheKey(request), CreateProcessContentRequest());
Channel<BackgroundJobBase> channel = Channel.CreateUnbounded<BackgroundJobBase>();
channelHandler.Setup(x => x.AddRunner(It.IsAny<Func<Channel<BackgroundJobBase>, Task>>()))
.Callback<Func<Channel<BackgroundJobBase>, Task>>(callback => runner = callback);
purviewClient.Setup(x => x.GetProtectionScopesAsync(It.IsAny<ProtectionScopesRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ProtectionScopesResponse { Scopes = [] });
_ = new BackgroundJobRunner(channelHandler.Object, purviewClient.Object, cacheProvider.Object, NullLogger.Instance, settings);
// Act
Assert.NotNull(runner);
await channel.Writer.WriteAsync(job);
channel.Writer.Complete();
await runner(channel);
// Assert
channelHandler.Verify(x => x.QueueJob(It.IsAny<ContentActivityJob>()), Times.Once);
}
#endregion
#region Helper Methods
private static ProtectionScopesRequest CreateProtectionScopesRequest()
{
return new ProtectionScopesRequest("user-123", "tenant-123")
{
Activities = ProtectionScopeActivities.UploadText,
Locations =
[
new("microsoft.graph.policyLocationApplication", "app-123")
]
};
}
private static ProcessContentRequest CreateProcessContentRequest()
{
PurviewTextContent content = new("Test content");
ProcessConversationMetadata metadata = new(content, "msg-123", false, "Test message", "test-correlation-id");
ActivityMetadata activityMetadata = new(Activity.UploadText);
DeviceMetadata deviceMetadata = new()
{
OperatingSystemSpecifications = new()
{
OperatingSystemPlatform = "Windows",
OperatingSystemVersion = "10"
}
};
IntegratedAppMetadata integratedAppMetadata = new()
{
Name = "TestApp",
Version = "1.0"
};
PolicyLocation policyLocation = new("microsoft.graph.policyLocationApplication", "app-123");
ProtectedAppMetadata protectedAppMetadata = new(policyLocation)
{
Name = "TestApp",
Version = "1.0"
};
ContentToProcess contentToProcess = new(
[metadata],
activityMetadata,
deviceMetadata,
integratedAppMetadata,
protectedAppMetadata);
return new ProcessContentRequest(contentToProcess, "user-123", "tenant-123");
}
private static PurviewSettings CreateValidPurviewSettings()
{
return new PurviewSettings("TestApp")
{
TenantId = "tenant-123",
PurviewAppLocation = new PurviewAppLocation(PurviewLocationType.Application, "app-123")
};
}
#endregion
}