// 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 System.Reflection;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
namespace Aspire.Hosting.Tests;
[Trait("Partition", "5")]
public class WithEndpointTests
{
// copied from /src/Shared/StringComparers.cs to avoid ambiguous reference since StringComparers exists internally in multiple Hosting assemblies.
private static StringComparison EndpointAnnotationName => StringComparison.OrdinalIgnoreCase;
[Fact]
public void EndpointIsProxiedBinaryCompatibilityOverloadsExist()
{
Assert.NotNull(GetPublicStaticMethod(
typeof(ResourceBuilderExtensions),
nameof(ResourceBuilderExtensions.WithEndpoint),
typeof(IResourceBuilder<>),
typeof(int?),
typeof(int?),
typeof(string),
typeof(string),
typeof(string),
typeof(bool),
typeof(bool?),
typeof(ProtocolType?)));
Assert.NotNull(GetPublicStaticMethod(
typeof(ResourceBuilderExtensions),
nameof(ResourceBuilderExtensions.WithEndpoint),
typeof(IResourceBuilder<>),
typeof(int?),
typeof(int?),
typeof(string),
typeof(string),
typeof(string),
typeof(bool),
typeof(bool?)));
Assert.NotNull(GetPublicStaticMethod(
typeof(ResourceBuilderExtensions),
nameof(ResourceBuilderExtensions.WithHttpEndpoint),
typeof(IResourceBuilder<>),
typeof(int?),
typeof(int?),
typeof(string),
typeof(string),
typeof(bool)));
Assert.NotNull(GetPublicStaticMethod(
typeof(ResourceBuilderExtensions),
nameof(ResourceBuilderExtensions.WithHttpsEndpoint),
typeof(IResourceBuilder<>),
typeof(int?),
typeof(int?),
typeof(string),
typeof(string),
typeof(bool)));
var withEndpointProxySupport = GetPublicStaticMethod(
typeof(ContainerResourceBuilderExtensions),
nameof(ContainerResourceBuilderExtensions.WithEndpointProxySupport),
typeof(IResourceBuilder<>),
typeof(bool));
Assert.NotNull(withEndpointProxySupport);
var constraint = Assert.Single(withEndpointProxySupport.GetGenericArguments()[0].GetGenericParameterConstraints());
Assert.Equal(typeof(ContainerResource), constraint);
Assert.NotNull(GetPublicConstructor(
typeof(EndpointAnnotation),
typeof(ProtocolType),
typeof(string),
typeof(string),
typeof(string),
typeof(int?),
typeof(int?),
typeof(bool?),
typeof(bool)));
Assert.NotNull(GetPublicConstructor(
typeof(EndpointAnnotation),
typeof(ProtocolType),
typeof(NetworkIdentifier),
typeof(string),
typeof(string),
typeof(string),
typeof(int?),
typeof(int?),
typeof(bool?),
typeof(bool)));
var isProxiedProperty = typeof(EndpointAnnotation).GetProperty(nameof(EndpointAnnotation.IsProxied), BindingFlags.Public | BindingFlags.Instance);
Assert.NotNull(isProxiedProperty);
Assert.Equal(typeof(bool), isProxiedProperty.PropertyType);
}
[Fact]
public void EndpointAnnotationConstructorOmissionLeavesProxySettingUnset()
{
var endpoint = new EndpointAnnotation(ProtocolType.Tcp);
Assert.True(endpoint.IsProxied);
Assert.Null(endpoint.IsExplicitlyProxied);
}
[Fact]
public void EndpointAnnotationBoolCompatibilityConstructorPreservesExplicitProxySetting()
{
var constructor = GetPublicConstructor(
typeof(EndpointAnnotation),
typeof(ProtocolType),
typeof(string),
typeof(string),
typeof(string),
typeof(int?),
typeof(int?),
typeof(bool?),
typeof(bool));
var endpoint = Assert.IsType<EndpointAnnotation>(constructor!.Invoke([ProtocolType.Tcp, "http", null, "http", null, null, null, true]));
Assert.True(endpoint.IsProxied);
Assert.True(endpoint.IsExplicitlyProxied);
}
[Fact]
public void WithHttpEndpointOmittedIsProxiedLeavesProxySettingUnset()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("app", "image")
.WithHttpEndpoint(name: "http");
var endpoint = Assert.Single(container.Resource.Annotations.OfType<EndpointAnnotation>());
Assert.True(endpoint.IsProxied);
Assert.Null(endpoint.IsExplicitlyProxied);
}
[Fact]
public void WithHttpEndpointBoolCompatibilityOverloadPreservesExplicitProxySetting()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("app", "image");
ResourceBuilderExtensions.WithHttpEndpoint(container, port: null, targetPort: null, name: "http", env: null, isProxied: true);
var endpoint = Assert.Single(container.Resource.Annotations.OfType<EndpointAnnotation>());
Assert.True(endpoint.IsProxied);
Assert.True(endpoint.IsExplicitlyProxied);
}
[Fact]
public void WithEndpointInvokesCallback()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithEndpoint(3000, 1000, name: "mybinding")
.WithEndpoint("mybinding", endpoint =>
{
endpoint.Port = 2000;
});
var endpoint = projectA.Resource.Annotations.OfType<EndpointAnnotation>()
.Where(e => string.Equals(e.Name, "mybinding", EndpointAnnotationName)).Single();
Assert.Equal(2000, endpoint.Port);
}
private static MethodInfo? GetPublicStaticMethod(Type declaringType, string methodName, params Type[] parameterTypes)
{
return declaringType.GetMethods(BindingFlags.Public | BindingFlags.Static)
.SingleOrDefault(method =>
method.Name == methodName &&
method.GetParameters().Select(parameter => NormalizeGenericParameterType(parameter.ParameterType)).SequenceEqual(parameterTypes));
}
private static ConstructorInfo? GetPublicConstructor(Type declaringType, params Type[] parameterTypes)
{
return declaringType.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
.SingleOrDefault(constructor =>
constructor.GetParameters().Select(parameter => parameter.ParameterType).SequenceEqual(parameterTypes));
}
private static Type NormalizeGenericParameterType(Type parameterType)
{
return parameterType.IsGenericType && parameterType.GetGenericTypeDefinition() == typeof(IResourceBuilder<>)
? typeof(IResourceBuilder<>)
: parameterType;
}
[Fact]
public void WithEndpointMakesTargetPortEqualToPortIfProxyless()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithEndpoint("mybinding", endpoint =>
{
endpoint.Port = 2000;
endpoint.IsProxied = false;
});
var endpoint = projectA.Resource.Annotations.OfType<EndpointAnnotation>()
.Where(e => string.Equals(e.Name, "mybinding", EndpointAnnotationName)).Single();
// It should fall back to the Port value since TargetPort was not set
Assert.Equal(2000, endpoint.TargetPort);
// In Proxy mode, the fallback should not happen
endpoint.IsProxied = true;
Assert.Null(endpoint.TargetPort);
// Back in proxy-less mode, it should fall back again
endpoint.IsProxied = false;
Assert.Equal(2000, endpoint.TargetPort);
// Setting it to null explicitly should disable the override mechanism
endpoint.TargetPort = null;
Assert.Null(endpoint.TargetPort);
// No fallback when setting TargetPort explicitly
endpoint.TargetPort = 2001;
Assert.Equal(2001, endpoint.TargetPort);
}
[Fact]
public void WithEndpointMakesPortEqualToTargetPortIfProxyless()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithEndpoint("mybinding", endpoint =>
{
endpoint.TargetPort = 2000;
endpoint.IsProxied = false;
});
var endpoint = projectA.Resource.Annotations.OfType<EndpointAnnotation>()
.Where(e => string.Equals(e.Name, "mybinding", EndpointAnnotationName)).Single();
// It should fall back to the TargetPort value since Port was not set
Assert.Equal(2000, endpoint.Port);
// In Proxy mode, the fallback should not happen
endpoint.IsProxied = true;
Assert.Null(endpoint.Port);
// Back in proxy-less mode, it should fall back again
endpoint.IsProxied = false;
Assert.Equal(2000, endpoint.Port);
// Setting it to null explicitly should disable the override mechanism
endpoint.Port = null;
Assert.Null(endpoint.Port);
// No fallback when setting Port explicitly
endpoint.Port = 2001;
Assert.Equal(2001, endpoint.Port);
}
[Fact]
public void WithEndpointCallbackDoesNotRunIfEndpointDoesntExistAndCreateIfNotExistsIsFalse()
{
var executed = false;
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithEndpoint("mybinding", endpoint =>
{
executed = true;
},
createIfNotExists: false);
Assert.False(executed);
Assert.False(projectA.Resource.TryGetAnnotationsOfType<EndpointAnnotation>(out var annotations));
}
[Fact]
public void WithEndpointCallbackRunsIfEndpointDoesntExistAndCreateIfNotExistsIsDefault()
{
var executed = false;
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithEndpoint("mybinding", endpoint =>
{
executed = true;
});
Assert.True(executed);
Assert.True(projectA.Resource.TryGetAnnotationsOfType<EndpointAnnotation>(out _));
}
[Fact]
public void WithEndpointCallbackRunsIfEndpointDoesntExistAndCreateIfNotExistsIsTrue()
{
var executed = false;
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta").WithEndpoint("mybinding", endpoint =>
{
executed = true;
},
createIfNotExists: true);
Assert.True(executed);
Assert.True(projectA.Resource.TryGetAnnotationsOfType<EndpointAnnotation>(out _));
}
[Fact]
public void WithHttpEndpointCallbackCreatesHttpEndpointWhenMissing()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("app", "image")
.WithHttpEndpointCallback(_ => { }, name: "callback-http");
var endpoint = container.Resource.Annotations.OfType<EndpointAnnotation>()
.Single(endpoint => string.Equals(endpoint.Name, "callback-http", EndpointAnnotationName));
Assert.Equal("http", endpoint.UriScheme);
Assert.Equal("http", endpoint.Transport);
Assert.Equal(ProtocolType.Tcp, endpoint.Protocol);
}
[Fact]
public void WithHttpsEndpointCallbackCreatesHttpsEndpointWhenMissing()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("app", "image")
.WithHttpsEndpointCallback(_ => { }, name: "callback-https");
var endpoint = container.Resource.Annotations.OfType<EndpointAnnotation>()
.Single(endpoint => string.Equals(endpoint.Name, "callback-https", EndpointAnnotationName));
Assert.Equal("https", endpoint.UriScheme);
Assert.Equal("http", endpoint.Transport);
Assert.Equal(ProtocolType.Tcp, endpoint.Protocol);
}
[Fact]
public void EndpointsWithTwoPortsSameNameUpdatesExisting()
{
using var builder = TestDistributedApplicationBuilder.Create();
builder.AddProject<ProjectA>("projecta")
.WithHttpsEndpoint(3000, 1000, name: "mybinding")
.WithHttpsEndpoint(3000, 2000, name: "mybinding");
var resource = Assert.Single(builder.Resources.OfType<ProjectResource>());
var endpoint = Assert.Single(resource.Annotations.OfType<EndpointAnnotation>(), e => e.Name == "mybinding");
Assert.Equal(3000, endpoint.Port);
Assert.Equal(2000, endpoint.TargetPort);
}
[Fact]
public void AddingTwoEndpointsWithDefaultNamesUpdatesExisting()
{
using var builder = TestDistributedApplicationBuilder.Create();
builder.AddProject<ProjectA>("projecta")
.WithHttpsEndpoint(3000, 1000)
.WithHttpsEndpoint(3000, 2000);
var resource = Assert.Single(builder.Resources.OfType<ProjectResource>());
var endpoint = Assert.Single(resource.Annotations.OfType<EndpointAnnotation>(), e => e.Name == "https");
Assert.Equal(3000, endpoint.Port);
Assert.Equal(2000, endpoint.TargetPort);
}
[Fact]
public void EndpointsWithSinglePortSameNameUpdatesExisting()
{
using var builder = TestDistributedApplicationBuilder.Create();
builder.AddProject<ProjectB>("projectb")
.WithHttpsEndpoint(1000, name: "mybinding")
.WithHttpsEndpoint(2000, name: "mybinding");
var resource = Assert.Single(builder.Resources.OfType<ProjectResource>());
var endpoint = Assert.Single(resource.Annotations.OfType<EndpointAnnotation>(), e => e.Name == "mybinding");
Assert.Equal(2000, endpoint.Port);
}
[Fact]
public async Task CanAddEndpointsWithContainerPortAndEnv()
{
using var builder = TestDistributedApplicationBuilder.Create();
builder.AddExecutable("foo", "foo", ".")
.WithHttpEndpoint(targetPort: 3001, name: "mybinding", env: "PORT");
using var app = builder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var exeResources = appModel.GetExecutableResources();
var resource = Assert.Single(exeResources);
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal("foo", resource.Name);
var endpoints = resource.Annotations.OfType<EndpointAnnotation>().ToArray();
Assert.Single(endpoints);
Assert.Equal("mybinding", endpoints[0].Name);
Assert.Equal(3001, endpoints[0].TargetPort);
Assert.Equal("http", endpoints[0].UriScheme);
Assert.Equal("3001", config["PORT"]);
}
[Fact]
public void WithExternalHttpEndpointsMarkExistingHttpEndpointsAsExternal()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("app", "image")
.WithEndpoint(name: "ep0")
.WithHttpEndpoint(name: "ep1")
.WithHttpsEndpoint(name: "ep2")
.WithExternalHttpEndpoints();
var ep0 = container.GetEndpoint("ep0");
var ep1 = container.GetEndpoint("ep1");
var ep2 = container.GetEndpoint("ep2");
Assert.False(ep0.EndpointAnnotation.IsExternal);
Assert.True(ep1.EndpointAnnotation.IsExternal);
Assert.True(ep2.EndpointAnnotation.IsExternal);
}
// Existing code...
[Fact]
public async Task VerifyManifestWithBothDifferentPortAndTargetPort()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("app", "image")
.WithEndpoint(name: "ep0", port: 8080, targetPort: 3000);
var manifest = await ManifestUtils.GetManifest(container.Resource).DefaultTimeout();
var expectedManifest =
"""
{
"type": "container.v0",
"image": "image:latest",
"bindings": {
"ep0": {
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
"port": 8080,
"targetPort": 3000
}
}
}
""";
Assert.Equal(expectedManifest, manifest.ToString());
}
[Fact]
public async Task VerifyManifestWithHttpPortWithTargetPort()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("app", "image")
.WithHttpEndpoint(name: "h1", targetPort: 3001);
var manifest = await ManifestUtils.GetManifest(container.Resource).DefaultTimeout();
var expectedManifest =
"""
{
"type": "container.v0",
"image": "image:latest",
"bindings": {
"h1": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 3001
}
}
}
""";
Assert.Equal(expectedManifest, manifest.ToString());
}
[Fact]
public async Task VerifyManifestWithHttpsAndTargetPort()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("app", "image")
.WithHttpsEndpoint(name: "h2", targetPort: 3001);
var manifest = await ManifestUtils.GetManifest(container.Resource).DefaultTimeout();
var expectedManifest =
"""
{
"type": "container.v0",
"image": "image:latest",
"bindings": {
"h2": {
"scheme": "https",
"protocol": "tcp",
"transport": "http",
"targetPort": 3001
}
}
}
""";
Assert.Equal(expectedManifest, manifest.ToString());
}
[Fact]
public async Task VerifyManifestContainerWithHttpEndpointAndNoPortsAllocatesPort()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("app", "image")
.WithHttpEndpoint(name: "h3");
var manifest = await ManifestUtils.GetManifest(container.Resource).DefaultTimeout();
var expectedManifest =
"""
{
"type": "container.v0",
"image": "image:latest",
"bindings": {
"h3": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 8000
}
}
}
""";
Assert.Equal(expectedManifest, manifest.ToString());
}
[Fact]
public async Task VerifyManifestContainerWithHttpsEndpointAllocatesPort()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("app", "image")
.WithHttpsEndpoint(name: "h4");
var manifest = await ManifestUtils.GetManifest(container.Resource).DefaultTimeout();
var expectedManifest =
"""
{
"type": "container.v0",
"image": "image:latest",
"bindings": {
"h4": {
"scheme": "https",
"protocol": "tcp",
"transport": "http",
"targetPort": 8000
}
}
}
""";
Assert.Equal(expectedManifest, manifest.ToString());
}
[Fact]
public async Task VerifyManifestWithHttpEndpointAndPortOnlySetsTargetPort()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("app", "image")
.WithHttpEndpoint(name: "otlp", port: 1004);
var manifest = await ManifestUtils.GetManifest(container.Resource).DefaultTimeout();
var expectedManifest =
"""
{
"type": "container.v0",
"image": "image:latest",
"bindings": {
"otlp": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 1004
}
}
}
""";
Assert.Equal(expectedManifest, manifest.ToString());
}
[Fact]
public async Task VerifyManifestWithTcpEndpointAndNoPortAllocatesPort()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("app", "image")
.WithEndpoint(name: "custom");
var manifest = await ManifestUtils.GetManifest(container.Resource).DefaultTimeout();
var expectedManifest =
"""
{
"type": "container.v0",
"image": "image:latest",
"bindings": {
"custom": {
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
"targetPort": 8000
}
}
}
""";
Assert.Equal(expectedManifest, manifest.ToString());
}
[Fact]
public async Task VerifyManifestProjectWithDefaultHttpEndpointsDoesNotAllocatePort()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var project = builder.AddProject<TestProject>("proj")
.WithHttpEndpoint(name: "hp") // Won't get targetPort since it's the first http
.WithHttpEndpoint(name: "hp2") // Will get a targetPort
.WithHttpsEndpoint(name: "hps") // Won't get targetPort since it's the first https
.WithHttpsEndpoint(name: "hps2") // Will get a targetPort
.WithEndpoint(scheme: "tcp", name: "tcp0"); // Will get a targetPort
var manifest = await ManifestUtils.GetManifest(project.Resource).DefaultTimeout();
var expectedManifest =
"""
{
"type": "project.v0",
"path": "projectpath",
"env": {
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
"HTTP_PORTS": "{proj.bindings.hp.targetPort};{proj.bindings.hp2.targetPort}",
"HTTPS_PORTS": "{proj.bindings.hps.targetPort};{proj.bindings.hps2.targetPort}"
},
"bindings": {
"hp": {
"scheme": "http",
"protocol": "tcp",
"transport": "http"
},
"hp2": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 8000
},
"hps": {
"scheme": "https",
"protocol": "tcp",
"transport": "http"
},
"hps2": {
"scheme": "https",
"protocol": "tcp",
"transport": "http",
"targetPort": 8001
},
"tcp0": {
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
"targetPort": 8002
}
}
}
""";
Assert.Equal(expectedManifest, manifest.ToString());
}
[Fact]
public async Task VerifyManifestProjectWithEndpointsSetsPortsEnvVariables()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var project = builder.AddProject<TestProject>("proj")
.WithHttpEndpoint()
.WithHttpEndpoint(name: "hp1", port: 5001)
.WithHttpEndpoint(name: "hp2", port: 5002, targetPort: 5003)
.WithHttpEndpoint(name: "hp3", targetPort: 5004)
.WithHttpEndpoint(name: "hp4")
.WithHttpEndpoint(name: "dontinjectme")
.WithHttpsEndpoint()
.WithHttpsEndpoint(name: "hps1", port: 7001)
.WithHttpsEndpoint(name: "hps2", port: 7002, targetPort: 7003)
.WithHttpsEndpoint(name: "hps3", targetPort: 7004)
.WithHttpsEndpoint(name: "hps4", targetPort: 7005)
// Should not be included in HTTP_PORTS
.WithEndpointsInEnvironment(e => e.Name != "dontinjectme");
var manifest = await ManifestUtils.GetManifest(project.Resource).DefaultTimeout();
var expectedEnv =
"""
{
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
"HTTP_PORTS": "{proj.bindings.http.targetPort};{proj.bindings.hp1.targetPort};{proj.bindings.hp2.targetPort};{proj.bindings.hp3.targetPort};{proj.bindings.hp4.targetPort}",
"HTTPS_PORTS": "{proj.bindings.https.targetPort};{proj.bindings.hps1.targetPort};{proj.bindings.hps2.targetPort};{proj.bindings.hps3.targetPort};{proj.bindings.hps4.targetPort}"
}
""";
Assert.Equal(expectedEnv, manifest["env"]!.ToString());
}
[Fact]
public async Task VerifyManifestPortAllocationIsGlobal()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container0 = builder.AddContainer("app0", "image")
.WithEndpoint(name: "custom");
var container1 = builder.AddContainer("app1", "image")
.WithEndpoint(name: "custom");
var manifests = await ManifestUtils.GetManifests([container0.Resource, container1.Resource]).DefaultTimeout();
var expectedManifest0 =
"""
{
"type": "container.v0",
"image": "image:latest",
"bindings": {
"custom": {
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
"targetPort": 8000
}
}
}
""";
var expectedManifest1 =
"""
{
"type": "container.v0",
"image": "image:latest",
"bindings": {
"custom": {
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
"targetPort": 8001
}
}
}
""";
Assert.Equal(expectedManifest0, manifests[0].ToString());
Assert.Equal(expectedManifest1, manifests[1].ToString());
}
[Fact]
public void WithEndpoint_WithAllArguments_ForwardsAllArguments()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithEndpoint(123, 456, "scheme", "mybinding", "env", true, true);
var endpoint = projectA.Resource.Annotations.OfType<EndpointAnnotation>()
.Where(e => string.Equals(e.Name, "mybinding", EndpointAnnotationName)).Single();
Assert.Equal(123, endpoint.Port);
Assert.Equal(456, endpoint.TargetPort);
Assert.Equal("scheme", endpoint.UriScheme);
Assert.Equal("env", endpoint.TargetPortEnvironmentVariable);
Assert.True(endpoint.IsProxied);
Assert.True(endpoint.IsExternal);
Assert.Equal(System.Net.Sockets.ProtocolType.Tcp, endpoint.Protocol);
}
[Fact]
public async Task LocalhostTopLevelDomainSetsAnnotationValues()
{
using var builder = TestDistributedApplicationBuilder.Create();
var tcs = new TaskCompletionSource();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithHttpsEndpoint()
.WithEndpoint("https", e => e.TargetHost = "example.localhost", createIfNotExists: false)
.OnBeforeResourceStarted((_, _, _) =>
{
tcs.SetResult();
return Task.CompletedTask;
});
var app = await builder.BuildAsync();
await app.StartAsync();
await tcs.Task;
var urls = projectA.Resource.Annotations.OfType<ResourceUrlAnnotation>();
Assert.Collection(urls,
url => Assert.StartsWith("https://example.localhost:", url.Url),
url => Assert.StartsWith("https://localhost:", url.Url));
EndpointAnnotation endpoint = Assert.Single(projectA.Resource.Annotations.OfType<EndpointAnnotation>());
Assert.NotNull(endpoint.AllocatedEndpoint);
Assert.Equal(EndpointBindingMode.SingleAddress, endpoint.AllocatedEndpoint.BindingMode);
Assert.Equal("localhost", endpoint.AllocatedEndpoint.Address);
await app.StopAsync();
}
[Theory]
[InlineData("0.0.0.0", EndpointBindingMode.IPv4AnyAddresses)]
//[InlineData("::", EndpointBindingMode.IPv6AnyAddresses)] // Need to figure out a good way to check that Ipv6 binding is supported
public async Task TopLevelDomainSetsAnnotationValues(string host, EndpointBindingMode endpointBindingMode)
{
using var builder = TestDistributedApplicationBuilder.Create();
var tcs = new TaskCompletionSource();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithHttpsEndpoint()
.WithEndpoint("https", e => e.TargetHost = host, createIfNotExists: false)
.OnBeforeResourceStarted((_, _, _) =>
{
tcs.SetResult();
return Task.CompletedTask;
});
var app = await builder.BuildAsync();
await app.StartAsync();
await tcs.Task;
var urls = projectA.Resource.Annotations.OfType<ResourceUrlAnnotation>();
Assert.Collection(urls,
url => Assert.StartsWith("https://localhost:", url.Url),
url => Assert.StartsWith($"https://{Environment.MachineName}:", url.Url));
EndpointAnnotation endpoint = Assert.Single(projectA.Resource.Annotations.OfType<EndpointAnnotation>());
Assert.NotNull(endpoint.AllocatedEndpoint);
Assert.Equal(endpointBindingMode, endpoint.AllocatedEndpoint.BindingMode);
Assert.Equal("localhost", endpoint.AllocatedEndpoint.Address);
await app.StopAsync();
}
[Fact]
public async Task VerifyManifestProjectWithExplicitPortAndNoLaunchProfile()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var project = builder.AddProject<TestProjectNoLaunchSettings>("proj", launchProfileName: null)
.WithHttpEndpoint(port: 5001);
var manifest = await ManifestUtils.GetManifest(project.Resource).DefaultTimeout();
var expectedManifest =
"""
{
"type": "project.v0",
"path": "projectpath",
"env": {
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
"HTTP_PORTS": "{proj.bindings.http.targetPort}"
},
"bindings": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"port": 5001
}
}
}
""";
Assert.Equal(expectedManifest, manifest.ToString());
}
private sealed class TestProject : IProjectMetadata
{
public string ProjectPath => "projectpath";
public LaunchSettings? LaunchSettings { get; } = new();
}
private sealed class TestProjectNoLaunchSettings : IProjectMetadata
{
public string ProjectPath => "projectpath";
public LaunchSettings? LaunchSettings => null;
}
[Fact]
public void WithHttpEndpointUpdatesPortOnExistingEndpoint()
{
var builder = DistributedApplication.CreateBuilder();
builder.AddContainer("mycontainer", "myimage")
.WithHttpEndpoint()
.WithHttpEndpoint(port: 5000);
using var app = builder.Build();
var resource = Assert.Single(builder.Resources.OfType<ContainerResource>());
var endpoint = Assert.Single(resource.Annotations.OfType<EndpointAnnotation>(), e => e.Name == "http");
Assert.Equal(5000, endpoint.Port);
}
[Fact]
public void WithHttpsEndpointUpdatesPortOnExistingEndpoint()
{
var builder = DistributedApplication.CreateBuilder();
builder.AddContainer("mycontainer", "myimage")
.WithHttpsEndpoint()
.WithHttpsEndpoint(port: 5001);
using var app = builder.Build();
var resource = Assert.Single(builder.Resources.OfType<ContainerResource>());
var endpoint = Assert.Single(resource.Annotations.OfType<EndpointAnnotation>(), e => e.Name == "https");
Assert.Equal(5001, endpoint.Port);
}
[Fact]
public void WithHttpEndpointUpdatePreservesExistingTargetPort()
{
var builder = DistributedApplication.CreateBuilder();
builder.AddContainer("mycontainer", "myimage")
.WithHttpEndpoint(targetPort: 8080)
.WithHttpEndpoint(port: 5000);
using var app = builder.Build();
var resource = Assert.Single(builder.Resources.OfType<ContainerResource>());
var endpoint = Assert.Single(resource.Annotations.OfType<EndpointAnnotation>(), e => e.Name == "http");
Assert.Equal(5000, endpoint.Port);
Assert.Equal(8080, endpoint.TargetPort);
}
[Fact]
public void WithHttpEndpointUpdatePreservesExistingEnvVar()
{
var builder = DistributedApplication.CreateBuilder();
builder.AddContainer("mycontainer", "myimage")
.WithHttpEndpoint(env: "PORT")
.WithHttpEndpoint(port: 3000);
using var app = builder.Build();
var resource = Assert.Single(builder.Resources.OfType<ContainerResource>());
var endpoint = Assert.Single(resource.Annotations.OfType<EndpointAnnotation>(), e => e.Name == "http");
Assert.Equal(3000, endpoint.Port);
Assert.Equal("PORT", endpoint.TargetPortEnvironmentVariable);
}
[Fact]
public void WithHttpEndpointUpdatePreservesIsProxiedFalse()
{
var builder = DistributedApplication.CreateBuilder();
builder.AddContainer("mycontainer", "myimage")
.WithHttpEndpoint(port: 8080, isProxied: false)
.WithHttpEndpoint(targetPort: 3000);
using var app = builder.Build();
var resource = Assert.Single(builder.Resources.OfType<ContainerResource>());
var endpoint = Assert.Single(resource.Annotations.OfType<EndpointAnnotation>(), e => e.Name == "http");
Assert.Equal(8080, endpoint.Port);
Assert.Equal(3000, endpoint.TargetPort);
Assert.False(endpoint.IsProxied);
}
[Fact]
public void WithHttpEndpointUpdateDoesNotDuplicateEnvAnnotation()
{
var builder = DistributedApplication.CreateBuilder();
builder.AddContainer("mycontainer", "myimage")
.WithHttpEndpoint(env: "PORT")
.WithHttpEndpoint(port: 3000, env: "PORT");
using var app = builder.Build();
var resource = Assert.Single(builder.Resources.OfType<ContainerResource>());
var envAnnotations = resource.Annotations.OfType<EnvironmentCallbackAnnotation>().ToList();
// Should have exactly one env callback for PORT, not two
Assert.Single(envAnnotations);
}
[Fact]
public async Task WithHttpEndpointUpdateReplacesEnvVar()
{
using var builder = TestDistributedApplicationBuilder.Create();
builder.AddExecutable("foo", "foo", ".")
.WithHttpEndpoint(targetPort: 3001, env: "PORT")
.WithHttpEndpoint(env: "NEW_PORT");
using var app = builder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var resource = Assert.Single(appModel.GetExecutableResources());
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
// NEW_PORT should be set, PORT should not
Assert.True(config.ContainsKey("NEW_PORT"));
Assert.False(config.ContainsKey("PORT"));
}
[Fact]
public void WithEndpointUpdateDoesNotChangeScheme()
{
var builder = DistributedApplication.CreateBuilder();
builder.AddContainer("mycontainer", "myimage")
.WithHttpEndpoint(name: "api")
.WithHttpsEndpoint(name: "api", port: 8443);
using var app = builder.Build();
var resource = Assert.Single(builder.Resources.OfType<ContainerResource>());
var endpoint = Assert.Single(resource.Annotations.OfType<EndpointAnnotation>(), e => e.Name == "api");
// Scheme should remain "http" from the first call
Assert.Equal("http", endpoint.UriScheme);
Assert.Equal(8443, endpoint.Port);
}
[Fact]
public void WithEndpointUpdateCanSetIsProxiedToTrue()
{
var builder = DistributedApplication.CreateBuilder();
builder.AddContainer("mycontainer", "myimage")
.WithHttpEndpoint(port: 8080, isProxied: false)
.WithHttpEndpoint(port: 9090, isProxied: true);
using var app = builder.Build();
var resource = Assert.Single(builder.Resources.OfType<ContainerResource>());
var endpoint = Assert.Single(resource.Annotations.OfType<EndpointAnnotation>(), e => e.Name == "http");
Assert.Equal(9090, endpoint.Port);
Assert.True(endpoint.IsProxied);
}
[Fact]
public void WithEndpointUpdateCanSetIsProxiedToFalse()
{
var builder = DistributedApplication.CreateBuilder();
builder.AddContainer("mycontainer", "myimage")
.WithHttpEndpoint(port: 8080)
.WithHttpEndpoint(isProxied: false);
using var app = builder.Build();
var resource = Assert.Single(builder.Resources.OfType<ContainerResource>());
var endpoint = Assert.Single(resource.Annotations.OfType<EndpointAnnotation>(), e => e.Name == "http");
Assert.Equal(8080, endpoint.Port);
Assert.False(endpoint.IsProxied);
}
[Fact]
public void WithEndpointUpdateChangesIsExternal()
{
var builder = DistributedApplication.CreateBuilder();
builder.AddContainer("mycontainer", "myimage")
.WithEndpoint(scheme: "http", name: "http")
.WithEndpoint(name: "http", isExternal: true);
using var app = builder.Build();
var resource = Assert.Single(builder.Resources.OfType<ContainerResource>());
var endpoint = Assert.Single(resource.Annotations.OfType<EndpointAnnotation>(), e => e.Name == "http");
Assert.True(endpoint.IsExternal);
}
private sealed class ProjectA : IProjectMetadata
{
public string ProjectPath => "projectA";
public LaunchSettings LaunchSettings { get; } = new();
}
private sealed class ProjectB : IProjectMetadata
{
public string ProjectPath => "projectB";
public LaunchSettings LaunchSettings { get; } = new();
}
}