| File: ComputeEnvironmentEndpointResolverTests.cs | Web Access |
| Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Sockets; using Aspire.Hosting.Utils; using Microsoft.AspNetCore.InternalTesting; namespace Aspire.Hosting.Tests; public class ComputeEnvironmentEndpointResolverTests { [Fact] public async Task OwningResourceInDifferentEnvironment_DelegatesToOwningEnvironment() { using var builder = TestDistributedApplicationBuilder.Create(); var currentEnv = builder.AddResource(new TestComputeEnvironmentResource("current")); var owningEnv = builder.AddResource(new TestComputeEnvironmentResource("owning")); var agent = builder.AddResource(new TestComputeResource("agent")); var endpoint = AddHttpEndpoint(agent.Resource, port: 8080, targetPort: 5000); agent.Resource.Annotations.Add(new DeploymentTargetAnnotation(owningEnv.Resource) { ComputeEnvironment = owningEnv.Resource }); var resolved = ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( endpoint.Property(EndpointProperty.Url), [currentEnv.Resource], out var expression); Assert.True(resolved); Assert.NotNull(expression); // The owning environment (TestComputeEnvironmentResource) maps the host to "{name}.example.com". Assert.Equal("http://agent.example.com:8080", await expression.GetValueAsync(default).DefaultTimeout()); } [Fact] public async Task EndpointReferenceOverload_DelegatesToOwningEnvironment() { using var builder = TestDistributedApplicationBuilder.Create(); var currentEnv = builder.AddResource(new TestComputeEnvironmentResource("current")); var owningEnv = builder.AddResource(new TestComputeEnvironmentResource("owning")); var agent = builder.AddResource(new TestComputeResource("agent")); var endpoint = AddHttpEndpoint(agent.Resource, port: 8080, targetPort: 5000); agent.Resource.Annotations.Add(new DeploymentTargetAnnotation(owningEnv.Resource) { ComputeEnvironment = owningEnv.Resource }); // The EndpointReference overload uses EndpointProperty.Url internally. var resolved = ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( endpoint, [currentEnv.Resource], out var expression); Assert.True(resolved); Assert.NotNull(expression); Assert.Equal("http://agent.example.com:8080", await expression.GetValueAsync(default).DefaultTimeout()); } [Fact] public void OwningResourceDeploysToCurrentEnvironment_ReturnsFalse() { using var builder = TestDistributedApplicationBuilder.Create(); var currentEnv = builder.AddResource(new TestComputeEnvironmentResource("current")); var agent = builder.AddResource(new TestComputeResource("agent")); var endpoint = AddHttpEndpoint(agent.Resource, port: 8080, targetPort: 5000); agent.Resource.Annotations.Add(new DeploymentTargetAnnotation(currentEnv.Resource) { ComputeEnvironment = currentEnv.Resource }); var resolved = ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( endpoint.Property(EndpointProperty.Url), [currentEnv.Resource], out var expression); Assert.False(resolved); Assert.Null(expression); } [Fact] public void OwningResourceBoundToCurrentEnvironmentWithoutDeploymentTarget_ReturnsFalseViaBackstop() { using var builder = TestDistributedApplicationBuilder.Create(); var currentEnv = builder.AddResource(new TestComputeEnvironmentResource("current")); // Bind via WithComputeEnvironment only (no DeploymentTargetAnnotation). The effective // environment resolves to the current environment and the ReferenceEquals backstop catches it. var agent = builder.AddResource(new TestComputeResource("agent")) .WithComputeEnvironment(currentEnv); var endpoint = AddHttpEndpoint(agent.Resource, port: 8080, targetPort: 5000); var resolved = ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( endpoint.Property(EndpointProperty.Url), [currentEnv.Resource], out var expression); Assert.False(resolved); Assert.Null(expression); } [Fact] public void OwningResourceHasNoComputeEnvironment_ReturnsFalse() { using var builder = TestDistributedApplicationBuilder.Create(); var currentEnv = builder.AddResource(new TestComputeEnvironmentResource("current")); // No binding and no deployment target: nothing to delegate to. var agent = builder.AddResource(new TestComputeResource("agent")); var endpoint = AddHttpEndpoint(agent.Resource, port: 8080, targetPort: 5000); var resolved = ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( endpoint.Property(EndpointProperty.Url), [currentEnv.Resource], out var expression); Assert.False(resolved); Assert.Null(expression); } [Fact] public void OwningResourceBoundToCurrentEnvironmentWithMultipleDeploymentTargets_ReturnsFalseAndDoesNotThrow() { using var builder = TestDistributedApplicationBuilder.Create(); var currentEnv = builder.AddResource(new TestComputeEnvironmentResource("current")); var otherEnv = builder.AddResource(new TestComputeEnvironmentResource("other")); // Explicit binding to the current environment plus deployment targets for both environments. // The binding lets GetComputeEnvironment() short-circuit to the current environment without // hitting the parameterless deployment-target resolver that throws on multi-target ambiguity. var agent = builder.AddResource(new TestComputeResource("agent")) .WithComputeEnvironment(currentEnv); agent.Resource.Annotations.Add(new DeploymentTargetAnnotation(currentEnv.Resource) { ComputeEnvironment = currentEnv.Resource }); agent.Resource.Annotations.Add(new DeploymentTargetAnnotation(otherEnv.Resource) { ComputeEnvironment = otherEnv.Resource }); var endpoint = AddHttpEndpoint(agent.Resource, port: 8080, targetPort: 5000); var resolved = ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( endpoint.Property(EndpointProperty.Url), [currentEnv.Resource], out var expression); Assert.False(resolved); Assert.Null(expression); } [Fact] public void OwningResourceUnboundWithMultipleDeploymentTargets_Throws() { using var builder = TestDistributedApplicationBuilder.Create(); var currentEnv = builder.AddResource(new TestComputeEnvironmentResource("current")); var otherEnv = builder.AddResource(new TestComputeEnvironmentResource("other")); // No binding: an unbound resource with more than one deployment target is ambiguous. // TryGetEffectiveComputeEnvironment falls back to the parameterless GetDeploymentTargetAnnotation() // which throws. This documents that the resolver does not swallow that ambiguity; the pipeline // rejects this configuration earlier in practice. var agent = builder.AddResource(new TestComputeResource("agent")); agent.Resource.Annotations.Add(new DeploymentTargetAnnotation(currentEnv.Resource) { ComputeEnvironment = currentEnv.Resource }); agent.Resource.Annotations.Add(new DeploymentTargetAnnotation(otherEnv.Resource) { ComputeEnvironment = otherEnv.Resource }); var endpoint = AddHttpEndpoint(agent.Resource, port: 8080, targetPort: 5000); Assert.Throws<InvalidOperationException>(() => ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( endpoint.Property(EndpointProperty.Url), [currentEnv.Resource], out _)); } private static EndpointReference AddHttpEndpoint(TestComputeResource resource, int? port, int? targetPort) { var endpoint = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http", port: port, targetPort: targetPort); resource.Annotations.Add(endpoint); return new EndpointReference(resource, endpoint); } private sealed class TestComputeEnvironmentResource(string name) : Resource(name), IComputeEnvironmentResource { #pragma warning disable ASPIRECOMPUTE002 public ReferenceExpression GetHostAddressExpression(EndpointReference endpointReference) => ReferenceExpression.Create($"{endpointReference.Resource.Name}.example.com"); #pragma warning restore ASPIRECOMPUTE002 } private sealed class TestComputeResource(string name) : Resource(name), IComputeResource, IResourceWithEndpoints { } }