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
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:
@@ -0,0 +1,184 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Aspire.Hosting.AgentFramework.DevUI.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="AgentEntityInfo"/> record.
|
||||
/// </summary>
|
||||
public class AgentEntityInfoTests
|
||||
{
|
||||
#region Constructor Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the Id property is set from the constructor parameter.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Constructor_WithId_SetsIdProperty()
|
||||
{
|
||||
// Arrange & Act
|
||||
var info = new AgentEntityInfo("test-agent");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test-agent", info.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the Description property is set when provided.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Constructor_WithDescription_SetsDescriptionProperty()
|
||||
{
|
||||
// Arrange & Act
|
||||
var info = new AgentEntityInfo("test-agent", "A test agent");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("A test agent", info.Description);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the Description property is null when not provided.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Constructor_WithoutDescription_DescriptionIsNull()
|
||||
{
|
||||
// Arrange & Act
|
||||
var info = new AgentEntityInfo("test-agent");
|
||||
|
||||
// Assert
|
||||
Assert.Null(info.Description);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Default Value Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Name defaults to the Id value when not explicitly set.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Name_NotSet_DefaultsToId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var info = new AgentEntityInfo("test-agent");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test-agent", info.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Name can be overridden with a custom value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Name_Set_ReturnsCustomValue()
|
||||
{
|
||||
// Arrange & Act
|
||||
var info = new AgentEntityInfo("test-agent") { Name = "Custom Name" };
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Custom Name", info.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Type defaults to "agent".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Type_NotSet_DefaultsToAgent()
|
||||
{
|
||||
// Arrange & Act
|
||||
var info = new AgentEntityInfo("test-agent");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("agent", info.Type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Type can be overridden with a custom value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Type_Set_ReturnsCustomValue()
|
||||
{
|
||||
// Arrange & Act
|
||||
var info = new AgentEntityInfo("test-agent") { Type = "workflow" };
|
||||
|
||||
// Assert
|
||||
Assert.Equal("workflow", info.Type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Framework defaults to "agent_framework".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Framework_NotSet_DefaultsToAgentFramework()
|
||||
{
|
||||
// Arrange & Act
|
||||
var info = new AgentEntityInfo("test-agent");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("agent_framework", info.Framework);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Framework can be overridden with a custom value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Framework_Set_ReturnsCustomValue()
|
||||
{
|
||||
// Arrange & Act
|
||||
var info = new AgentEntityInfo("test-agent") { Framework = "custom_framework" };
|
||||
|
||||
// Assert
|
||||
Assert.Equal("custom_framework", info.Framework);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Record Equality Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that two AgentEntityInfo records with identical values are equal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equality_SameValues_AreEqual()
|
||||
{
|
||||
// Arrange
|
||||
var info1 = new AgentEntityInfo("agent", "description");
|
||||
var info2 = new AgentEntityInfo("agent", "description");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(info1, info2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that two AgentEntityInfo records with different Ids are not equal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equality_DifferentIds_AreNotEqual()
|
||||
{
|
||||
// Arrange
|
||||
var info1 = new AgentEntityInfo("agent1");
|
||||
var info2 = new AgentEntityInfo("agent2");
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(info1, info2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that with-expression creates a modified copy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithExpression_ModifiesProperty_CreatesNewInstance()
|
||||
{
|
||||
// Arrange
|
||||
var original = new AgentEntityInfo("agent", "Original description");
|
||||
|
||||
// Act
|
||||
var modified = original with { Description = "Modified description" };
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Original description", original.Description);
|
||||
Assert.Equal("Modified description", modified.Description);
|
||||
Assert.Equal(original.Id, modified.Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+567
@@ -0,0 +1,567 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Aspire.Hosting.ApplicationModel;
|
||||
using Moq;
|
||||
|
||||
namespace Aspire.Hosting.AgentFramework.DevUI.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="AgentFrameworkBuilderExtensions"/> class.
|
||||
/// </summary>
|
||||
public class AgentFrameworkBuilderExtensionsTests
|
||||
{
|
||||
#region AddDevUI Validation Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddDevUI throws ArgumentNullException when builder is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddDevUI_NullBuilder_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<ArgumentNullException>(
|
||||
() => AgentFrameworkBuilderExtensions.AddDevUI(null!, "devui"));
|
||||
Assert.Equal("builder", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddDevUI throws ArgumentNullException when name is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddDevUI_NullName_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var builder = DistributedApplication.CreateBuilder();
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<ArgumentNullException>(
|
||||
() => builder.AddDevUI(null!));
|
||||
Assert.Equal("name", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddDevUI creates a resource with the specified name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddDevUI_ValidName_CreatesResourceWithName()
|
||||
{
|
||||
// Arrange
|
||||
var builder = DistributedApplication.CreateBuilder();
|
||||
|
||||
// Act
|
||||
var resourceBuilder = builder.AddDevUI("my-devui");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("my-devui", resourceBuilder.Resource.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddDevUI creates a DevUIResource.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddDevUI_ReturnsDevUIResourceBuilder()
|
||||
{
|
||||
// Arrange
|
||||
var builder = DistributedApplication.CreateBuilder();
|
||||
|
||||
// Act
|
||||
var resourceBuilder = builder.AddDevUI("devui");
|
||||
|
||||
// Assert
|
||||
Assert.IsType<DevUIResource>(resourceBuilder.Resource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddDevUI with port configures the endpoint.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddDevUI_WithPort_ConfiguresEndpointWithPort()
|
||||
{
|
||||
// Arrange
|
||||
var builder = DistributedApplication.CreateBuilder();
|
||||
|
||||
// Act
|
||||
var resourceBuilder = builder.AddDevUI("devui", port: 8090);
|
||||
|
||||
// Assert
|
||||
var endpoint = resourceBuilder.Resource.Annotations
|
||||
.OfType<EndpointAnnotation>()
|
||||
.FirstOrDefault(e => e.Name == "http");
|
||||
Assert.NotNull(endpoint);
|
||||
Assert.Equal(8090, endpoint.Port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddDevUI without port leaves port as null for dynamic allocation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddDevUI_WithoutPort_EndpointHasDynamicPort()
|
||||
{
|
||||
// Arrange
|
||||
var builder = DistributedApplication.CreateBuilder();
|
||||
|
||||
// Act
|
||||
var resourceBuilder = builder.AddDevUI("devui");
|
||||
|
||||
// Assert
|
||||
var endpoint = resourceBuilder.Resource.Annotations
|
||||
.OfType<EndpointAnnotation>()
|
||||
.FirstOrDefault(e => e.Name == "http");
|
||||
Assert.NotNull(endpoint);
|
||||
Assert.Null(endpoint.Port);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region WithAgentService Validation Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAgentService throws ArgumentNullException when builder is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_NullBuilder_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var mockAgentService = CreateMockAgentServiceBuilder(appBuilder, "agent-service");
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<ArgumentNullException>(
|
||||
() => AgentFrameworkBuilderExtensions.WithAgentService(null!, mockAgentService));
|
||||
Assert.Equal("builder", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAgentService throws ArgumentNullException when agentService is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_NullAgentService_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devuiBuilder = appBuilder.AddDevUI("devui");
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<ArgumentNullException>(
|
||||
() => devuiBuilder.WithAgentService<IResourceWithEndpoints>(null!));
|
||||
Assert.Equal("agentService", exception.ParamName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region WithAgentService Annotation Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAgentService adds an AgentServiceAnnotation to the resource.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_ValidService_AddsAnnotation()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devuiBuilder = appBuilder.AddDevUI("devui");
|
||||
var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent");
|
||||
|
||||
// Act
|
||||
devuiBuilder.WithAgentService(agentService);
|
||||
|
||||
// Assert
|
||||
var annotation = devuiBuilder.Resource.Annotations
|
||||
.OfType<AgentServiceAnnotation>()
|
||||
.FirstOrDefault();
|
||||
Assert.NotNull(annotation);
|
||||
Assert.Same(agentService.Resource, annotation.AgentService);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAgentService defaults to agent name being the resource name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_NoAgents_DefaultsToResourceNameAsAgent()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devuiBuilder = appBuilder.AddDevUI("devui");
|
||||
var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent");
|
||||
|
||||
// Act
|
||||
devuiBuilder.WithAgentService(agentService);
|
||||
|
||||
// Assert
|
||||
var annotation = devuiBuilder.Resource.Annotations
|
||||
.OfType<AgentServiceAnnotation>()
|
||||
.First();
|
||||
Assert.Single(annotation.Agents);
|
||||
Assert.Equal("writer-agent", annotation.Agents[0].Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAgentService with explicit agents uses those agents.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_WithAgents_UsesProvidedAgents()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devuiBuilder = appBuilder.AddDevUI("devui");
|
||||
var agentService = CreateMockAgentServiceBuilder(appBuilder, "multi-agent-service");
|
||||
var agents = new[]
|
||||
{
|
||||
new AgentEntityInfo("agent1", "First agent"),
|
||||
new AgentEntityInfo("agent2", "Second agent")
|
||||
};
|
||||
|
||||
// Act
|
||||
devuiBuilder.WithAgentService(agentService, agents: agents);
|
||||
|
||||
// Assert
|
||||
var annotation = devuiBuilder.Resource.Annotations
|
||||
.OfType<AgentServiceAnnotation>()
|
||||
.First();
|
||||
Assert.Equal(2, annotation.Agents.Count);
|
||||
Assert.Equal("agent1", annotation.Agents[0].Id);
|
||||
Assert.Equal("agent2", annotation.Agents[1].Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAgentService with custom prefix uses that prefix.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_WithEntityIdPrefix_UsesProvidedPrefix()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devuiBuilder = appBuilder.AddDevUI("devui");
|
||||
var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent");
|
||||
|
||||
// Act
|
||||
devuiBuilder.WithAgentService(agentService, entityIdPrefix: "custom-prefix");
|
||||
|
||||
// Assert
|
||||
var annotation = devuiBuilder.Resource.Annotations
|
||||
.OfType<AgentServiceAnnotation>()
|
||||
.First();
|
||||
Assert.Equal("custom-prefix", annotation.EntityIdPrefix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAgentService without prefix leaves EntityIdPrefix null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_NoEntityIdPrefix_PrefixIsNull()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devuiBuilder = appBuilder.AddDevUI("devui");
|
||||
var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent");
|
||||
|
||||
// Act
|
||||
devuiBuilder.WithAgentService(agentService);
|
||||
|
||||
// Assert
|
||||
var annotation = devuiBuilder.Resource.Annotations
|
||||
.OfType<AgentServiceAnnotation>()
|
||||
.First();
|
||||
Assert.Null(annotation.EntityIdPrefix);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Chaining Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAgentService returns the builder for chaining.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_ReturnsSameBuilder_ForChaining()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devuiBuilder = appBuilder.AddDevUI("devui");
|
||||
var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent");
|
||||
|
||||
// Act
|
||||
var result = devuiBuilder.WithAgentService(agentService);
|
||||
|
||||
// Assert
|
||||
Assert.Same(devuiBuilder, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that multiple WithAgentService calls can be chained.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_MultipleCalls_AddsMultipleAnnotations()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devuiBuilder = appBuilder.AddDevUI("devui");
|
||||
var writerService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent");
|
||||
var editorService = CreateMockAgentServiceBuilder(appBuilder, "editor-agent");
|
||||
|
||||
// Act
|
||||
devuiBuilder
|
||||
.WithAgentService(writerService)
|
||||
.WithAgentService(editorService);
|
||||
|
||||
// Assert
|
||||
var annotations = devuiBuilder.Resource.Annotations
|
||||
.OfType<AgentServiceAnnotation>()
|
||||
.ToList();
|
||||
Assert.Equal(2, annotations.Count);
|
||||
Assert.Contains(annotations, a => a.AgentService.Name == "writer-agent");
|
||||
Assert.Contains(annotations, a => a.AgentService.Name == "editor-agent");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddDevUI returns a builder that can be chained with WithAgentService.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddDevUI_CanChainWithAgentService()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent");
|
||||
|
||||
// Act - Chain AddDevUI with WithAgentService
|
||||
var result = appBuilder.AddDevUI("devui").WithAgentService(agentService);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
var annotation = result.Resource.Annotations
|
||||
.OfType<AgentServiceAnnotation>()
|
||||
.FirstOrDefault();
|
||||
Assert.NotNull(annotation);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Relationship Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAgentService creates a relationship annotation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_CreatesRelationshipAnnotation()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devuiBuilder = appBuilder.AddDevUI("devui");
|
||||
var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent");
|
||||
|
||||
// Act
|
||||
devuiBuilder.WithAgentService(agentService);
|
||||
|
||||
// Assert
|
||||
var relationship = devuiBuilder.Resource.Annotations
|
||||
.OfType<ResourceRelationshipAnnotation>()
|
||||
.FirstOrDefault();
|
||||
Assert.NotNull(relationship);
|
||||
Assert.Equal("agent-backend", relationship.Type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that multiple WithAgentService calls create multiple relationship annotations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_MultipleCalls_CreatesMultipleRelationships()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devuiBuilder = appBuilder.AddDevUI("devui");
|
||||
var writerService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent");
|
||||
var editorService = CreateMockAgentServiceBuilder(appBuilder, "editor-agent");
|
||||
|
||||
// Act
|
||||
devuiBuilder
|
||||
.WithAgentService(writerService)
|
||||
.WithAgentService(editorService);
|
||||
|
||||
// Assert
|
||||
var relationships = devuiBuilder.Resource.Annotations
|
||||
.OfType<ResourceRelationshipAnnotation>()
|
||||
.ToList();
|
||||
Assert.Equal(2, relationships.Count);
|
||||
Assert.All(relationships, r => Assert.Equal("agent-backend", r.Type));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Agent Metadata Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that agent description is preserved when specified.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_AgentWithDescription_PreservesDescription()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devuiBuilder = appBuilder.AddDevUI("devui");
|
||||
var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent");
|
||||
var agents = new[] { new AgentEntityInfo("writer", "Writes creative stories") };
|
||||
|
||||
// Act
|
||||
devuiBuilder.WithAgentService(agentService, agents: agents);
|
||||
|
||||
// Assert
|
||||
var annotation = devuiBuilder.Resource.Annotations
|
||||
.OfType<AgentServiceAnnotation>()
|
||||
.First();
|
||||
Assert.Equal("Writes creative stories", annotation.Agents[0].Description);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that custom agent properties are preserved.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_CustomAgentProperties_ArePreserved()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devuiBuilder = appBuilder.AddDevUI("devui");
|
||||
var agentService = CreateMockAgentServiceBuilder(appBuilder, "custom-service");
|
||||
var agents = new[]
|
||||
{
|
||||
new AgentEntityInfo("custom-agent")
|
||||
{
|
||||
Name = "Custom Display Name",
|
||||
Type = "workflow",
|
||||
Framework = "custom_framework"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
devuiBuilder.WithAgentService(agentService, agents: agents);
|
||||
|
||||
// Assert
|
||||
var annotation = devuiBuilder.Resource.Annotations
|
||||
.OfType<AgentServiceAnnotation>()
|
||||
.First();
|
||||
var agent = annotation.Agents[0];
|
||||
Assert.Equal("custom-agent", agent.Id);
|
||||
Assert.Equal("Custom Display Name", agent.Name);
|
||||
Assert.Equal("workflow", agent.Type);
|
||||
Assert.Equal("custom_framework", agent.Framework);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that empty agents array can be explicitly provided and is respected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_EmptyAgentsArray_UsesEmptyArray()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devuiBuilder = appBuilder.AddDevUI("devui");
|
||||
var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent");
|
||||
var emptyAgents = Array.Empty<AgentEntityInfo>();
|
||||
|
||||
// Act
|
||||
devuiBuilder.WithAgentService(agentService, agents: emptyAgents);
|
||||
|
||||
// Assert
|
||||
var annotation = devuiBuilder.Resource.Annotations
|
||||
.OfType<AgentServiceAnnotation>()
|
||||
.First();
|
||||
// When explicitly passing an empty array, the extension method respects it
|
||||
// This is the expected behavior - explicit empty means "discover at runtime"
|
||||
Assert.Empty(annotation.Agents);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Case Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddDevUI can be called multiple times with different names.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddDevUI_MultipleCalls_CreatesSeparateResources()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
|
||||
// Act
|
||||
var devui1 = appBuilder.AddDevUI("devui1");
|
||||
var devui2 = appBuilder.AddDevUI("devui2");
|
||||
|
||||
// Assert
|
||||
Assert.NotSame(devui1.Resource, devui2.Resource);
|
||||
Assert.Equal("devui1", devui1.Resource.Name);
|
||||
Assert.Equal("devui2", devui2.Resource.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that same agent service can be added to multiple DevUI resources.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_SameServiceToMultipleDevUI_Works()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devui1 = appBuilder.AddDevUI("devui1");
|
||||
var devui2 = appBuilder.AddDevUI("devui2");
|
||||
var agentService = CreateMockAgentServiceBuilder(appBuilder, "shared-agent");
|
||||
|
||||
// Act
|
||||
devui1.WithAgentService(agentService);
|
||||
devui2.WithAgentService(agentService);
|
||||
|
||||
// Assert
|
||||
var annotation1 = devui1.Resource.Annotations.OfType<AgentServiceAnnotation>().Single();
|
||||
var annotation2 = devui2.Resource.Annotations.OfType<AgentServiceAnnotation>().Single();
|
||||
Assert.Same(annotation1.AgentService, annotation2.AgentService);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAgentService works with different entity ID prefixes for the same service.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_DifferentPrefixesToDifferentDevUI_Works()
|
||||
{
|
||||
// Arrange
|
||||
var appBuilder = DistributedApplication.CreateBuilder();
|
||||
var devui1 = appBuilder.AddDevUI("devui1");
|
||||
var devui2 = appBuilder.AddDevUI("devui2");
|
||||
var agentService = CreateMockAgentServiceBuilder(appBuilder, "writer-agent");
|
||||
|
||||
// Act
|
||||
devui1.WithAgentService(agentService, entityIdPrefix: "prefix1");
|
||||
devui2.WithAgentService(agentService, entityIdPrefix: "prefix2");
|
||||
|
||||
// Assert
|
||||
var annotation1 = devui1.Resource.Annotations.OfType<AgentServiceAnnotation>().Single();
|
||||
var annotation2 = devui2.Resource.Annotations.OfType<AgentServiceAnnotation>().Single();
|
||||
Assert.Equal("prefix1", annotation1.EntityIdPrefix);
|
||||
Assert.Equal("prefix2", annotation2.EntityIdPrefix);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock agent service builder for testing.
|
||||
/// Uses a minimal resource implementation that satisfies IResourceWithEndpoints.
|
||||
/// </summary>
|
||||
private static IResourceBuilder<IResourceWithEndpoints> CreateMockAgentServiceBuilder(
|
||||
IDistributedApplicationBuilder appBuilder,
|
||||
string name)
|
||||
{
|
||||
// Create a mock resource that implements IResourceWithEndpoints
|
||||
var mockResource = new Mock<IResourceWithEndpoints>();
|
||||
mockResource.Setup(r => r.Name).Returns(name);
|
||||
mockResource.Setup(r => r.Annotations).Returns(new ResourceAnnotationCollection());
|
||||
|
||||
var mockBuilder = new Mock<IResourceBuilder<IResourceWithEndpoints>>();
|
||||
mockBuilder.Setup(b => b.Resource).Returns(mockResource.Object);
|
||||
mockBuilder.Setup(b => b.ApplicationBuilder).Returns(appBuilder);
|
||||
|
||||
return mockBuilder.Object;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Aspire.Hosting.ApplicationModel;
|
||||
using Moq;
|
||||
|
||||
namespace Aspire.Hosting.AgentFramework.DevUI.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="AgentServiceAnnotation"/> class.
|
||||
/// </summary>
|
||||
public class AgentServiceAnnotationTests
|
||||
{
|
||||
#region Constructor Validation Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that passing null for agentService throws ArgumentNullException.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Constructor_NullAgentService_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new AgentServiceAnnotation(null!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a valid agentService can be used to create the annotation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Constructor_ValidAgentService_CreatesAnnotation()
|
||||
{
|
||||
// Arrange
|
||||
var mockResource = new Mock<IResource>();
|
||||
mockResource.Setup(r => r.Name).Returns("test-service");
|
||||
|
||||
// Act
|
||||
var annotation = new AgentServiceAnnotation(mockResource.Object);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(annotation);
|
||||
Assert.Same(mockResource.Object, annotation.AgentService);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AgentService property returns the value passed to constructor.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AgentService_ReturnsConstructorValue()
|
||||
{
|
||||
// Arrange
|
||||
var mockResource = new Mock<IResource>();
|
||||
mockResource.Setup(r => r.Name).Returns("my-service");
|
||||
|
||||
// Act
|
||||
var annotation = new AgentServiceAnnotation(mockResource.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Same(mockResource.Object, annotation.AgentService);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that EntityIdPrefix returns null when not specified.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EntityIdPrefix_NotSpecified_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var mockResource = new Mock<IResource>();
|
||||
mockResource.Setup(r => r.Name).Returns("test-service");
|
||||
|
||||
// Act
|
||||
var annotation = new AgentServiceAnnotation(mockResource.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Null(annotation.EntityIdPrefix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that EntityIdPrefix returns the value passed to constructor.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EntityIdPrefix_Specified_ReturnsValue()
|
||||
{
|
||||
// Arrange
|
||||
var mockResource = new Mock<IResource>();
|
||||
mockResource.Setup(r => r.Name).Returns("test-service");
|
||||
|
||||
// Act
|
||||
var annotation = new AgentServiceAnnotation(mockResource.Object, entityIdPrefix: "custom-prefix");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("custom-prefix", annotation.EntityIdPrefix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Agents returns empty collection when not specified.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Agents_NotSpecified_ReturnsEmptyCollection()
|
||||
{
|
||||
// Arrange
|
||||
var mockResource = new Mock<IResource>();
|
||||
mockResource.Setup(r => r.Name).Returns("test-service");
|
||||
|
||||
// Act
|
||||
var annotation = new AgentServiceAnnotation(mockResource.Object);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(annotation.Agents);
|
||||
Assert.Empty(annotation.Agents);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Agents returns the list passed to constructor.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Agents_Specified_ReturnsValue()
|
||||
{
|
||||
// Arrange
|
||||
var mockResource = new Mock<IResource>();
|
||||
mockResource.Setup(r => r.Name).Returns("test-service");
|
||||
var agents = new[] { new AgentEntityInfo("agent1"), new AgentEntityInfo("agent2") };
|
||||
|
||||
// Act
|
||||
var annotation = new AgentServiceAnnotation(mockResource.Object, agents: agents);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, annotation.Agents.Count);
|
||||
Assert.Equal("agent1", annotation.Agents[0].Id);
|
||||
Assert.Equal("agent2", annotation.Agents[1].Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Full Constructor Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that all constructor parameters are correctly stored.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Constructor_AllParameters_SetsAllProperties()
|
||||
{
|
||||
// Arrange
|
||||
var mockResource = new Mock<IResource>();
|
||||
mockResource.Setup(r => r.Name).Returns("full-service");
|
||||
var agents = new[] { new AgentEntityInfo("writer", "Writes stories") };
|
||||
|
||||
// Act
|
||||
var annotation = new AgentServiceAnnotation(
|
||||
mockResource.Object,
|
||||
entityIdPrefix: "writer-backend",
|
||||
agents: agents);
|
||||
|
||||
// Assert
|
||||
Assert.Same(mockResource.Object, annotation.AgentService);
|
||||
Assert.Equal("writer-backend", annotation.EntityIdPrefix);
|
||||
Assert.Single(annotation.Agents);
|
||||
Assert.Equal("writer", annotation.Agents[0].Id);
|
||||
Assert.Equal("Writes stories", annotation.Agents[0].Description);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Aspire.Hosting.AgentFramework.DevUI\Aspire.Hosting.AgentFramework.DevUI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+613
@@ -0,0 +1,613 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Aspire.Hosting.ApplicationModel;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Aspire.Hosting.AgentFramework.DevUI.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="DevUIAggregatorHostedService"/> class.
|
||||
/// </summary>
|
||||
public class DevUIAggregatorHostedServiceTests
|
||||
{
|
||||
#region RewriteAgentIdInQueryString Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that RewriteAgentIdInQueryString returns empty string when query string has no value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RewriteAgentIdInQueryString_EmptyQueryString_ReturnsEmptyString()
|
||||
{
|
||||
// Arrange
|
||||
var queryString = QueryString.Empty;
|
||||
|
||||
// Act
|
||||
var result = DevUIAggregatorHostedService.RewriteAgentIdInQueryString(queryString, "writer");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that RewriteAgentIdInQueryString rewrites agent_id to the un-prefixed value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RewriteAgentIdInQueryString_WithPrefixedAgentId_RewritesToUnprefixed()
|
||||
{
|
||||
// Arrange
|
||||
var queryString = new QueryString("?agent_id=writer-agent%2Fwriter");
|
||||
|
||||
// Act
|
||||
var result = DevUIAggregatorHostedService.RewriteAgentIdInQueryString(queryString, "writer");
|
||||
|
||||
// Assert
|
||||
Assert.Contains("agent_id=writer", result);
|
||||
Assert.DoesNotContain("writer-agent", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that RewriteAgentIdInQueryString preserves other query parameters.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RewriteAgentIdInQueryString_WithOtherParams_PreservesOtherParams()
|
||||
{
|
||||
// Arrange
|
||||
var queryString = new QueryString("?agent_id=writer-agent%2Fwriter&conversation_id=123&page=5");
|
||||
|
||||
// Act
|
||||
var result = DevUIAggregatorHostedService.RewriteAgentIdInQueryString(queryString, "writer");
|
||||
|
||||
// Assert
|
||||
Assert.Contains("agent_id=writer", result);
|
||||
Assert.Contains("conversation_id=123", result);
|
||||
Assert.Contains("page=5", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that RewriteAgentIdInQueryString works when agent_id is not the first parameter.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RewriteAgentIdInQueryString_AgentIdNotFirst_StillRewrites()
|
||||
{
|
||||
// Arrange
|
||||
var queryString = new QueryString("?page=1&agent_id=editor-agent%2Feditor&limit=10");
|
||||
|
||||
// Act
|
||||
var result = DevUIAggregatorHostedService.RewriteAgentIdInQueryString(queryString, "editor");
|
||||
|
||||
// Assert
|
||||
Assert.Contains("agent_id=editor", result);
|
||||
Assert.DoesNotContain("editor-agent", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that RewriteAgentIdInQueryString handles special characters in actual agent ID.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RewriteAgentIdInQueryString_SpecialCharsInAgentId_UrlEncodesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var queryString = new QueryString("?agent_id=prefix%2Fmy-agent");
|
||||
|
||||
// Act
|
||||
var result = DevUIAggregatorHostedService.RewriteAgentIdInQueryString(queryString, "my-agent");
|
||||
|
||||
// Assert
|
||||
// The result should contain the agent_id with the value properly encoded if needed
|
||||
Assert.Contains("agent_id=my-agent", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that RewriteAgentIdInQueryString handles an agent_id with no prefix.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RewriteAgentIdInQueryString_NoPrefix_SetsDirectly()
|
||||
{
|
||||
// Arrange
|
||||
var queryString = new QueryString("?agent_id=simple");
|
||||
|
||||
// Act
|
||||
var result = DevUIAggregatorHostedService.RewriteAgentIdInQueryString(queryString, "new-value");
|
||||
|
||||
// Assert
|
||||
Assert.Contains("agent_id=new-value", result);
|
||||
Assert.DoesNotContain("simple", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that RewriteAgentIdInQueryString adds agent_id even if not originally present.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RewriteAgentIdInQueryString_NoAgentId_AddsAgentId()
|
||||
{
|
||||
// Arrange
|
||||
var queryString = new QueryString("?page=1&limit=10");
|
||||
|
||||
// Act
|
||||
var result = DevUIAggregatorHostedService.RewriteAgentIdInQueryString(queryString, "writer");
|
||||
|
||||
// Assert
|
||||
Assert.Contains("agent_id=writer", result);
|
||||
Assert.Contains("page=1", result);
|
||||
Assert.Contains("limit=10", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that RewriteAgentIdInQueryString returns proper format starting with ?.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RewriteAgentIdInQueryString_ValidQuery_ReturnsQueryStringFormat()
|
||||
{
|
||||
// Arrange
|
||||
var queryString = new QueryString("?agent_id=test");
|
||||
|
||||
// Act
|
||||
var result = DevUIAggregatorHostedService.RewriteAgentIdInQueryString(queryString, "writer");
|
||||
|
||||
// Assert
|
||||
Assert.StartsWith("?", result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Backend Resolution Behavior Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that ResolveBackends returns empty dictionary when no annotations are present.
|
||||
/// These tests verify the expected behavior of the aggregator via the DevUI resource annotations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DevUIResource_NoAnnotations_ResolveBackendsReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var builder = DistributedApplication.CreateBuilder();
|
||||
var devui = builder.AddDevUI("devui");
|
||||
|
||||
// Act
|
||||
var annotations = devui.Resource.Annotations
|
||||
.OfType<AgentServiceAnnotation>()
|
||||
.ToList();
|
||||
|
||||
// Assert - no AgentServiceAnnotation means no backends
|
||||
Assert.Empty(annotations);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that WithAgentService adds proper annotations for backend resolution.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_AddsAnnotation_ForBackendResolution()
|
||||
{
|
||||
// Arrange
|
||||
var builder = DistributedApplication.CreateBuilder();
|
||||
var devui = builder.AddDevUI("devui");
|
||||
var agentService = CreateMockAgentServiceBuilder(builder, "writer-agent");
|
||||
|
||||
// Act
|
||||
devui.WithAgentService(agentService);
|
||||
|
||||
// Assert
|
||||
var annotation = devui.Resource.Annotations
|
||||
.OfType<AgentServiceAnnotation>()
|
||||
.FirstOrDefault();
|
||||
|
||||
Assert.NotNull(annotation);
|
||||
Assert.Equal("writer-agent", annotation.AgentService.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that custom EntityIdPrefix is properly stored in the annotation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_CustomPrefix_StoresInAnnotation()
|
||||
{
|
||||
// Arrange
|
||||
var builder = DistributedApplication.CreateBuilder();
|
||||
var devui = builder.AddDevUI("devui");
|
||||
var agentService = CreateMockAgentServiceBuilder(builder, "writer-agent");
|
||||
|
||||
// Act
|
||||
devui.WithAgentService(agentService, entityIdPrefix: "custom-writer");
|
||||
|
||||
// Assert
|
||||
var annotation = devui.Resource.Annotations
|
||||
.OfType<AgentServiceAnnotation>()
|
||||
.First();
|
||||
|
||||
Assert.Equal("custom-writer", annotation.EntityIdPrefix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that multiple agent services create multiple annotations for backend resolution.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithAgentService_MultipleServices_CreatesMultipleAnnotations()
|
||||
{
|
||||
// Arrange
|
||||
var builder = DistributedApplication.CreateBuilder();
|
||||
var devui = builder.AddDevUI("devui");
|
||||
var writerService = CreateMockAgentServiceBuilder(builder, "writer-agent");
|
||||
var editorService = CreateMockAgentServiceBuilder(builder, "editor-agent");
|
||||
|
||||
// Act
|
||||
devui.WithAgentService(writerService);
|
||||
devui.WithAgentService(editorService);
|
||||
|
||||
// Assert
|
||||
var annotations = devui.Resource.Annotations
|
||||
.OfType<AgentServiceAnnotation>()
|
||||
.ToList();
|
||||
|
||||
Assert.Equal(2, annotations.Count);
|
||||
Assert.Contains(annotations, a => a.AgentService.Name == "writer-agent");
|
||||
Assert.Contains(annotations, a => a.AgentService.Name == "editor-agent");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Backend Endpoint Selection Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that ResolveBackends prefers the HTTPS endpoint when both HTTP and HTTPS are allocated.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ResolveBackends_WithHttpAndHttpsEndpoints_PrefersHttps()
|
||||
{
|
||||
// Arrange
|
||||
var devui = new DevUIResource("devui");
|
||||
var agentService = new TestEndpointResource("writer-agent");
|
||||
AddAllocatedEndpoint(agentService, "http", "http", 5050);
|
||||
AddAllocatedEndpoint(agentService, "https", "https", 7443);
|
||||
devui.Annotations.Add(new AgentServiceAnnotation(agentService));
|
||||
var aggregator = new DevUIAggregatorHostedService(devui, NullLogger.Instance);
|
||||
|
||||
// Act
|
||||
var backends = aggregator.ResolveBackends();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("https://localhost:7443", backends["writer-agent"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that ResolveBackends falls back to HTTP when the HTTPS endpoint is not present.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ResolveBackends_WithOnlyHttpEndpoint_UsesHttp()
|
||||
{
|
||||
// Arrange
|
||||
var devui = new DevUIResource("devui");
|
||||
var agentService = new TestEndpointResource("writer-agent");
|
||||
AddAllocatedEndpoint(agentService, "http", "http", 5050);
|
||||
devui.Annotations.Add(new AgentServiceAnnotation(agentService));
|
||||
var aggregator = new DevUIAggregatorHostedService(devui, NullLogger.Instance);
|
||||
|
||||
// Act
|
||||
var backends = aggregator.ResolveBackends();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("http://localhost:5050", backends["writer-agent"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that ResolveBackends falls back to HTTP when the HTTPS endpoint has not been allocated yet.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ResolveBackends_WithUnallocatedHttpsEndpoint_UsesHttp()
|
||||
{
|
||||
// Arrange
|
||||
var devui = new DevUIResource("devui");
|
||||
var agentService = new TestEndpointResource("writer-agent");
|
||||
AddEndpoint(agentService, "https", "https");
|
||||
AddAllocatedEndpoint(agentService, "http", "http", 5050);
|
||||
devui.Annotations.Add(new AgentServiceAnnotation(agentService));
|
||||
var aggregator = new DevUIAggregatorHostedService(devui, NullLogger.Instance);
|
||||
|
||||
// Act
|
||||
var backends = aggregator.ResolveBackends();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("http://localhost:5050", backends["writer-agent"]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Entity ID Parsing Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the expected format for prefixed entity IDs in the aggregator.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("writer-agent/writer", "writer-agent", "writer")]
|
||||
[InlineData("editor-agent/editor", "editor-agent", "editor")]
|
||||
[InlineData("custom/my-agent", "custom", "my-agent")]
|
||||
[InlineData("prefix/sub/path", "prefix", "sub/path")]
|
||||
public void PrefixedEntityId_Format_ExtractsCorrectly(string prefixedId, string expectedPrefix, string expectedRest)
|
||||
{
|
||||
// This test documents the expected format for prefixed entity IDs
|
||||
// The aggregator uses "prefix/entityId" format where:
|
||||
// - prefix is typically the resource name or custom prefix
|
||||
// - entityId is the original entity identifier from the backend
|
||||
|
||||
// Act
|
||||
var slashIndex = prefixedId.IndexOf('/');
|
||||
var prefix = prefixedId[..slashIndex];
|
||||
var rest = prefixedId[(slashIndex + 1)..];
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedPrefix, prefix);
|
||||
Assert.Equal(expectedRest, rest);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock agent service builder for testing.
|
||||
/// Uses a minimal resource implementation that satisfies IResourceWithEndpoints.
|
||||
/// </summary>
|
||||
private static IResourceBuilder<IResourceWithEndpoints> CreateMockAgentServiceBuilder(
|
||||
IDistributedApplicationBuilder appBuilder,
|
||||
string name)
|
||||
{
|
||||
// Create a mock resource that implements IResourceWithEndpoints
|
||||
var mockResource = new Moq.Mock<IResourceWithEndpoints>();
|
||||
mockResource.Setup(r => r.Name).Returns(name);
|
||||
mockResource.Setup(r => r.Annotations).Returns(new ResourceAnnotationCollection());
|
||||
|
||||
var mockBuilder = new Moq.Mock<IResourceBuilder<IResourceWithEndpoints>>();
|
||||
mockBuilder.Setup(b => b.Resource).Returns(mockResource.Object);
|
||||
mockBuilder.Setup(b => b.ApplicationBuilder).Returns(appBuilder);
|
||||
|
||||
return mockBuilder.Object;
|
||||
}
|
||||
|
||||
private static void AddAllocatedEndpoint(
|
||||
TestEndpointResource resource,
|
||||
string name,
|
||||
string uriScheme,
|
||||
int port)
|
||||
{
|
||||
var endpoint = AddEndpoint(resource, name, uriScheme);
|
||||
endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", port);
|
||||
}
|
||||
|
||||
private static EndpointAnnotation AddEndpoint(
|
||||
TestEndpointResource resource,
|
||||
string name,
|
||||
string uriScheme)
|
||||
{
|
||||
var endpoint = new EndpointAnnotation(
|
||||
ProtocolType.Tcp,
|
||||
uriScheme: uriScheme,
|
||||
name: name,
|
||||
port: null,
|
||||
isProxied: false);
|
||||
|
||||
resource.Annotations.Add(endpoint);
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
private sealed class TestEndpointResource(string name) : Resource(name), IResourceWithEndpoints;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Proxy Target Validation Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://localhost:5000", "/v1/conversations")]
|
||||
[InlineData("http://localhost:5000", "/devui/index.html?v=1")]
|
||||
public void ValidateProxyTarget_TargetStaysOnConfiguredBackend_ReturnsTargetUri(string backendUrl, string path)
|
||||
{
|
||||
// Arrange
|
||||
var backendUri = new Uri(backendUrl);
|
||||
|
||||
// Act
|
||||
var target = DevUIAggregatorHostedService.ValidateProxyTarget(backendUrl, path);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(target);
|
||||
Assert.Equal(backendUri.Host, target!.Host);
|
||||
Assert.Equal(backendUri.Scheme, target.Scheme);
|
||||
Assert.Equal(backendUri.Port, target.Port);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://localhost:5000", "http://alternate.example/data")] // absolute path overrides the host
|
||||
[InlineData("http://localhost:5000", "//alternate.example/data")] // protocol-relative path overrides the host
|
||||
[InlineData("http://localhost:5000", "https://localhost:5000/data")] // scheme differs from the backend
|
||||
[InlineData("http://localhost:5000", "http://localhost:6000/data")] // port differs from the backend
|
||||
[InlineData("this is not a url", "/v1/conversations")] // malformed backend url
|
||||
public void ValidateProxyTarget_TargetLeavesConfiguredBackend_ReturnsNull(string backendUrl, string path)
|
||||
{
|
||||
// Act
|
||||
var target = DevUIAggregatorHostedService.ValidateProxyTarget(backendUrl, path);
|
||||
|
||||
// Assert
|
||||
Assert.Null(target);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProxyRequest_ConversationRoute_ForwardsToConfiguredBackendAsync()
|
||||
{
|
||||
// Arrange
|
||||
await using var proxy = await ProxyTestContext.StartAsync();
|
||||
|
||||
// Act
|
||||
var response = await proxy.SendAsync("/v1/conversations?limit=10");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var forwarded = Assert.Single(proxy.BackendRequests);
|
||||
Assert.Equal("/v1/conversations", forwarded.Path);
|
||||
Assert.Equal("?limit=10", forwarded.QueryString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProxyRequest_DevUIRoute_ForwardsToConfiguredBackendAsync()
|
||||
{
|
||||
// Arrange
|
||||
await using var proxy = await ProxyTestContext.StartAsync();
|
||||
|
||||
// Act
|
||||
var response = await proxy.SendAsync("/devui/index.html?v=1");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var forwarded = Assert.Single(proxy.BackendRequests);
|
||||
Assert.Equal("/devui/index.html", forwarded.Path);
|
||||
Assert.Equal("?v=1", forwarded.QueryString);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/v1/conversations/../conversations")]
|
||||
[InlineData("/devui/../devui/index.html")]
|
||||
public async Task ProxyRequest_NormalizedPath_ForwardsToConfiguredBackendAsync(string requestPath)
|
||||
{
|
||||
// Arrange
|
||||
await using var proxy = await ProxyTestContext.StartAsync();
|
||||
|
||||
// Act
|
||||
var response = await proxy.SendAsync(requestPath);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Single(proxy.BackendRequests);
|
||||
}
|
||||
|
||||
#region Proxy Test Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Hosts a stub backend together with a DevUI aggregator wired to it, and exposes an
|
||||
/// <see cref="HttpClient"/> targeting the aggregator so proxied requests can be observed
|
||||
/// on the backend.
|
||||
/// </summary>
|
||||
private sealed class ProxyTestContext : IAsyncDisposable
|
||||
{
|
||||
private readonly WebApplication _backend;
|
||||
private readonly DevUIAggregatorHostedService _aggregator;
|
||||
private readonly HttpClient _client;
|
||||
private readonly List<(string Path, string QueryString)> _backendRequests;
|
||||
|
||||
private ProxyTestContext(
|
||||
WebApplication backend,
|
||||
DevUIAggregatorHostedService aggregator,
|
||||
HttpClient client,
|
||||
List<(string Path, string QueryString)> backendRequests)
|
||||
{
|
||||
this._backend = backend;
|
||||
this._aggregator = aggregator;
|
||||
this._client = client;
|
||||
this._backendRequests = backendRequests;
|
||||
}
|
||||
|
||||
/// <summary>Gets the requests received by the stub backend, in arrival order.</summary>
|
||||
public IReadOnlyList<(string Path, string QueryString)> BackendRequests => this._backendRequests;
|
||||
|
||||
public static async Task<ProxyTestContext> StartAsync()
|
||||
{
|
||||
var backendRequests = new List<(string Path, string QueryString)>();
|
||||
var backend = await StartStubBackendAsync(backendRequests).ConfigureAwait(false);
|
||||
|
||||
var aggregator = await StartAggregatorAsync(GetBaseAddress(backend)).ConfigureAwait(false);
|
||||
var client = new HttpClient { BaseAddress = new Uri($"http://127.0.0.1:{aggregator.AllocatedPort}") };
|
||||
|
||||
return new ProxyTestContext(backend, aggregator, client, backendRequests);
|
||||
}
|
||||
|
||||
/// <summary>Sends a GET request to the aggregator using the given relative path.</summary>
|
||||
public Task<HttpResponseMessage> SendAsync(string relativePath)
|
||||
=> this._client.GetAsync(new Uri(relativePath, UriKind.Relative));
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
this._client.Dispose();
|
||||
await this._aggregator.DisposeAsync().ConfigureAwait(false);
|
||||
await this._backend.StopAsync().ConfigureAwait(false);
|
||||
await this._backend.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a minimal backend that records the path and query string of every request it receives.
|
||||
/// </summary>
|
||||
private static async Task<WebApplication> StartStubBackendAsync(List<(string Path, string QueryString)> requests)
|
||||
{
|
||||
var builder = WebApplication.CreateSlimBuilder();
|
||||
builder.Logging.ClearProviders();
|
||||
|
||||
var app = builder.Build();
|
||||
app.Urls.Add("http://127.0.0.1:0");
|
||||
app.Map("{**path}", (HttpContext context) =>
|
||||
{
|
||||
requests.Add((context.Request.Path.Value ?? string.Empty, context.Request.QueryString.Value ?? string.Empty));
|
||||
return Results.Json(new { ok = true });
|
||||
});
|
||||
|
||||
await app.StartAsync().ConfigureAwait(false);
|
||||
return app;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a DevUI aggregator configured with a single backend pointing at <paramref name="backendUrl"/>.
|
||||
/// </summary>
|
||||
private static async Task<DevUIAggregatorHostedService> StartAggregatorAsync(string backendUrl)
|
||||
{
|
||||
var resource = new DevUIResource("test-devui");
|
||||
resource.Annotations.Add(new AgentServiceAnnotation(CreateBackendResource(backendUrl)));
|
||||
|
||||
using var loggerFactory = LoggerFactory.Create(_ => { });
|
||||
var aggregator = new DevUIAggregatorHostedService(
|
||||
resource,
|
||||
loggerFactory.CreateLogger<DevUIAggregatorHostedService>());
|
||||
|
||||
await aggregator.StartAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
return aggregator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a backend resource whose "http" endpoint is allocated to <paramref name="backendUrl"/>.
|
||||
/// </summary>
|
||||
private static TestBackendResource CreateBackendResource(string backendUrl)
|
||||
{
|
||||
var backendUri = new Uri(backendUrl);
|
||||
var resource = new TestBackendResource("test-backend");
|
||||
|
||||
var endpoint = new EndpointAnnotation(
|
||||
ProtocolType.Tcp,
|
||||
uriScheme: "http",
|
||||
name: "http",
|
||||
port: backendUri.Port,
|
||||
isProxied: false)
|
||||
{
|
||||
TargetHost = backendUri.Host
|
||||
};
|
||||
endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, backendUri.Host, backendUri.Port);
|
||||
|
||||
resource.Annotations.Add(endpoint);
|
||||
return resource;
|
||||
}
|
||||
|
||||
private static string GetBaseAddress(WebApplication app)
|
||||
=> app.Services.GetRequiredService<IServer>().Features.Get<IServerAddressesFeature>()!.Addresses.First();
|
||||
|
||||
private sealed class TestBackendResource(string name) : Resource(name), IResourceWithEndpoints;
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using Aspire.Hosting.ApplicationModel;
|
||||
|
||||
namespace Aspire.Hosting.AgentFramework.DevUI.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="DevUIResource"/> class.
|
||||
/// </summary>
|
||||
public class DevUIResourceTests
|
||||
{
|
||||
#region Constructor Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the resource name is correctly set.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Constructor_WithName_SetsName()
|
||||
{
|
||||
// Arrange & Act
|
||||
var resource = new DevUIResource("test-devui");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test-devui", resource.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the resource implements IResourceWithEndpoints.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Resource_ImplementsIResourceWithEndpoints()
|
||||
{
|
||||
// Arrange & Act
|
||||
var resource = new DevUIResource("test-devui");
|
||||
|
||||
// Assert
|
||||
Assert.IsAssignableFrom<IResourceWithEndpoints>(resource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the resource implements IResourceWithWaitSupport.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Resource_ImplementsIResourceWithWaitSupport()
|
||||
{
|
||||
// Arrange & Act
|
||||
var resource = new DevUIResource("test-devui");
|
||||
|
||||
// Assert
|
||||
Assert.IsAssignableFrom<IResourceWithWaitSupport>(resource);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Endpoint Annotation Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the resource has an HTTP endpoint annotation when port is specified.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Constructor_WithPort_AddsEndpointAnnotation()
|
||||
{
|
||||
// Arrange & Act
|
||||
var resource = CreateResourceWithPort(8090);
|
||||
|
||||
// Assert
|
||||
var endpoint = resource.Annotations.OfType<EndpointAnnotation>().FirstOrDefault();
|
||||
Assert.NotNull(endpoint);
|
||||
Assert.Equal("http", endpoint.Name);
|
||||
Assert.Equal(8090, endpoint.Port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the endpoint annotation has correct protocol type.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EndpointAnnotation_HasTcpProtocol()
|
||||
{
|
||||
// Arrange
|
||||
var resource = CreateResourceWithPort(8080);
|
||||
|
||||
// Act
|
||||
var endpoint = resource.Annotations.OfType<EndpointAnnotation>().First();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ProtocolType.Tcp, endpoint.Protocol);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the endpoint annotation has HTTP URI scheme.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EndpointAnnotation_HasHttpUriScheme()
|
||||
{
|
||||
// Arrange
|
||||
var resource = CreateResourceWithPort(8080);
|
||||
|
||||
// Act
|
||||
var endpoint = resource.Annotations.OfType<EndpointAnnotation>().First();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("http", endpoint.UriScheme);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the endpoint is not proxied.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EndpointAnnotation_IsNotProxied()
|
||||
{
|
||||
// Arrange
|
||||
var resource = CreateResourceWithPort(8080);
|
||||
|
||||
// Act
|
||||
var endpoint = resource.Annotations.OfType<EndpointAnnotation>().First();
|
||||
|
||||
// Assert
|
||||
Assert.False(endpoint.IsProxied);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the endpoint target host is localhost.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EndpointAnnotation_TargetHostIsLocalhost()
|
||||
{
|
||||
// Arrange
|
||||
var resource = CreateResourceWithPort(8080);
|
||||
|
||||
// Act
|
||||
var endpoint = resource.Annotations.OfType<EndpointAnnotation>().First();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("localhost", endpoint.TargetHost);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the endpoint has no fixed port when null is passed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Constructor_WithNullPort_EndpointHasNullPort()
|
||||
{
|
||||
// Arrange & Act
|
||||
var resource = CreateResourceWithPort(null);
|
||||
|
||||
// Assert
|
||||
var endpoint = resource.Annotations.OfType<EndpointAnnotation>().FirstOrDefault();
|
||||
Assert.NotNull(endpoint);
|
||||
Assert.Null(endpoint.Port);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PrimaryEndpoint Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that PrimaryEndpoint returns an endpoint reference.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PrimaryEndpoint_ReturnsEndpointReference()
|
||||
{
|
||||
// Arrange
|
||||
var resource = CreateResourceWithPort(8080);
|
||||
|
||||
// Act
|
||||
var endpoint = resource.PrimaryEndpoint;
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(endpoint);
|
||||
Assert.Same(resource, endpoint.Resource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that PrimaryEndpoint returns the same instance on multiple calls.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PrimaryEndpoint_MultipleCalls_ReturnsSameInstance()
|
||||
{
|
||||
// Arrange
|
||||
var resource = CreateResourceWithPort(8080);
|
||||
|
||||
// Act
|
||||
var endpoint1 = resource.PrimaryEndpoint;
|
||||
var endpoint2 = resource.PrimaryEndpoint;
|
||||
|
||||
// Assert
|
||||
Assert.Same(endpoint1, endpoint2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static DevUIResource CreateResourceWithPort(int? port) => new("test-devui", port);
|
||||
}
|
||||
Reference in New Issue
Block a user