File: Codespaces\CodespacesUrlRewriterTests.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 Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;
using Xunit.Abstractions;
 
namespace Aspire.Hosting.Tests.Codespaces;
 
public class CodespacesUrlRewriterTests(ITestOutputHelper testOutputHelper)
{
    [Fact]
    public async Task VerifyUrlsRewriterStopsWhenNotInCodespaces()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
 
        // Explicitly disable codespace behavior for this test.
        builder.Configuration["CODESPACES"] = "false";
 
        builder.Services.AddLogging(logging =>
        {
            logging.AddFakeLogging();
            logging.AddXunit(testOutputHelper);
        });
 
        var resource = builder.AddResource(new CustomResource("resource"));
 
        var abortToken = new CancellationTokenSource(TimeSpan.FromSeconds(60));
 
        using var app = builder.Build();
        var rns = app.Services.GetRequiredService<ResourceNotificationService>();
 
        await app.StartAsync(abortToken.Token);
 
        var collector = app.Services.GetFakeLogCollector();
 
        var urlRewriterStopped = false;
 
        while (!abortToken.Token.IsCancellationRequested)
        {
            var logs = collector.GetSnapshot();
            urlRewriterStopped = logs.Any(l => l.Message.Contains("Not running in Codespaces, skipping URL rewriting."));
            if (urlRewriterStopped)
            {
                break;
            }
        }
 
        Assert.True(urlRewriterStopped);
 
        await app.StopAsync(abortToken.Token);
    }
 
    [Fact]
    public async Task VerifyUrlsRewrittenWhenInCodespaces()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
 
        builder.Configuration["CODESPACES"] = "true";
        builder.Configuration["GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"] = "app.github.dev";
        builder.Configuration["CODESPACE_NAME"] = "test-codespace";
 
        var resource = builder.AddResource(new CustomResource("resource"));
 
        var abortToken = new CancellationTokenSource(TimeSpan.FromSeconds(60));
 
        using var app = builder.Build();
        var rns = app.Services.GetRequiredService<ResourceNotificationService>();
 
        await app.StartAsync(abortToken.Token);
 
        // Push the URL to the resource state.
        var localhostUrlSnapshot = new UrlSnapshot("Test", "http://localhost:1234", false);
        await rns.PublishUpdateAsync(resource.Resource, s => s with
        {
            State = KnownResourceStates.Running,
            Urls = [localhostUrlSnapshot]
        });
 
        // Wait until
        var resourceEvent = await rns.WaitForResourceAsync(
            resource.Resource.Name,
            (re) => {
                var match = re.Snapshot.Urls.Length > 0 && re.Snapshot.Urls[0].Url.Contains("app.github.dev");
                return match;
            },
            abortToken.Token);
 
        Assert.Collection(
            resourceEvent.Snapshot.Urls,
            u =>
            {
                Assert.Equal("Test", u.Name);
                Assert.Equal("http://test-codespace-1234.app.github.dev/", u.Url);
                Assert.False(u.IsInternal);
            }
            );
 
        await app.StopAsync(abortToken.Token);
    }
 
    private sealed class CustomResource(string name) : Resource(name)
    {
    }
}