db620d33df
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
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
81 lines
3.7 KiB
C#
81 lines
3.7 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.ClientModel.Primitives;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
using Foundry.Hosting.IntegrationTests.Fixtures;
|
|
|
|
namespace Foundry.Hosting.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// End-to-end test for the per-user OAuth toolbox consent flow. The hosted container pre-registers a
|
|
/// Foundry toolbox whose tool source needs per-user OAuth consent; invoking the agent must surface an
|
|
/// <c>oauth_consent_request</c> (carrying a consent link) to the consumer instead of silently running
|
|
/// without the tool, and the container must stay routable (no 424) despite the consent-gated toolbox.
|
|
/// </summary>
|
|
[Trait("Category", "FoundryHostedAgents")]
|
|
public sealed class ToolboxOAuthConsentHostedAgentTests(ToolboxOAuthConsentHostedAgentFixture fixture)
|
|
: IClassFixture<ToolboxOAuthConsentHostedAgentFixture>
|
|
{
|
|
private readonly ToolboxOAuthConsentHostedAgentFixture _fixture = fixture;
|
|
|
|
[Fact(Skip = "Pending TestContainer build, a consent-gated toolbox in the IT project, and end to end smoke (step 5).")]
|
|
public async Task ToolRequiringConsent_SurfacesOAuthConsentRequestToConsumerAsync()
|
|
{
|
|
// Arrange: the agent is backed by a pre-registered toolbox whose tool source requires
|
|
// per-user OAuth consent (the fixture provisioned it, the container stayed routable).
|
|
var agent = this._fixture.Agent;
|
|
|
|
// Act: ask for something that needs the OAuth-protected tool. The toolbox proxy returns
|
|
// CONSENT_REQUIRED for the (unconsented) caller, which the hosted agent surfaces as an
|
|
// oauth_consent_request output item and marks the response incomplete.
|
|
var response = await agent.RunAsync(
|
|
"Use the OAuth-protected tool to act on my behalf. List my pull requests.");
|
|
|
|
// Assert: the consumer captured an oauth_consent_request carrying a usable https consent link.
|
|
// The high-level client exposes the (non-OpenAI) consent item as an AIContent whose
|
|
// RawRepresentation serializes to the oauth_consent_request wire shape, mirroring how the
|
|
// Hosted-Toolbox-AuthPaths REPL client detects it.
|
|
var consentLink = response.Messages
|
|
.SelectMany(m => m.Contents)
|
|
.Select(c => TryGetConsentLink(c.RawRepresentation))
|
|
.FirstOrDefault(link => link is not null);
|
|
|
|
Assert.False(string.IsNullOrWhiteSpace(consentLink),
|
|
"Expected the response to surface an oauth_consent_request with a consent link.");
|
|
Assert.StartsWith("https://", consentLink, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static string? TryGetConsentLink(object? raw)
|
|
{
|
|
if (raw is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
BinaryData json = ModelReaderWriter.Write(raw, new ModelReaderWriterOptions("J"));
|
|
using JsonDocument doc = JsonDocument.Parse(json);
|
|
JsonElement root = doc.RootElement;
|
|
if (root.ValueKind == JsonValueKind.Object
|
|
&& root.TryGetProperty("type", out JsonElement typeProp)
|
|
&& typeProp.GetString() == "oauth_consent_request"
|
|
&& root.TryGetProperty("consent_link", out JsonElement linkProp)
|
|
&& linkProp.GetString() is string link
|
|
&& !string.IsNullOrWhiteSpace(link))
|
|
{
|
|
return link;
|
|
}
|
|
}
|
|
catch (Exception ex) when (ex is JsonException or InvalidOperationException or NotSupportedException or FormatException)
|
|
{
|
|
// Not a persistable model, or no consent link present — treat as no consent.
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|