File: NativeSymbolicValueClientTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Native.Core.Tests\Aspire.Hosting.Native.Core.Tests.csproj (Aspire.Hosting.Native.Core.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.Native.Client;
using Aspire.Hosting.Native.Core.Tests.TestServices;
 
namespace Aspire.Hosting.Native.Core.Tests;
 
public class NativeSymbolicValueClientTests
{
    [Fact]
    public async Task SocketValuesKeepSecretsAndReferencesOnServerAndRejectCrossCompositionAuthority()
    {
        await using var host = await NativeClientTestHost.StartAsync();
        await using var client = await NativeClient.ConnectAsync(host.Endpoint, host.Token, TimeSpan.FromSeconds(10), TimeProvider.System);
        var session = await client.Server.OpenApplicationAsync();
        var composition = await session.StartGenerationAsync();
        var source = await composition.AddResourceAsync("cache", "example/redis");
        var target = await composition.AddResourceAsync("consumer", "example/executable");
        var password = await composition.AddSecretParameterAsync("password", "secret-value");
        var endpoint = await composition.CreateEndpointValueAsync(source, "tcp", "hostAndPort");
        var prefix = await composition.CreateLiteralValueAsync("redis://:");
        var uri = await prefix.ConcatValueAsync(await password.EncodeUriValueAsync());
        uri = await uri.ConcatValueAsync(await composition.CreateLiteralValueAsync("@"));
        uri = await uri.ConcatValueAsync(endpoint);
        await target.SetSymbolicConfigurationAsync("connection", uri);
        var metadata = Assert.Single((await target.InspectSymbolicConfigurationAsync()).Properties);
        Assert.Equal("connection", metadata.Name);
        Assert.True(metadata.IsSecret);
        Assert.Equal(["password"], metadata.ParameterReferences);
        Assert.Equal([(await source.InspectResourceAsync()).ResourceId], metadata.References);
        Assert.Empty((await target.InspectResourceAsync()).Dependencies);
        Assert.Empty((await target.InspectResourceAsync()).Configuration);
        var otherSession = await client.Server.OpenApplicationAsync();
        var other = await otherSession.StartGenerationAsync();
        var otherValue = await other.CreateLiteralValueAsync("other");
        var mismatch = await Assert.ThrowsAsync<NativeCapabilityException>(() => uri.ConcatValueAsync(otherValue));
        Assert.Equal("INVALID_ARGUMENT", mismatch.Code);
        await target.RemoveSymbolicConfigurationAsync("connection");
        Assert.Empty((await target.InspectResourceAsync()).Dependencies);
        await session.RetireGenerationAsync(composition);
        var retired = await Assert.ThrowsAsync<NativeCapabilityException>(() => password.EncodeUriValueAsync());
        Assert.Equal("HANDLE_NOT_FOUND", retired.Code);
        await otherSession.RetireGenerationAsync(other);
    }
 
    [Fact]
    public async Task WorkspaceValuesRebindRevisionsAndRejectStaleLaunchesWithoutInspectingSecrets()
    {
        await using var host = await NativeClientTestHost.StartAsync();
        await using var client = await NativeClient.ConnectAsync(host.Endpoint, host.Token, TimeSpan.FromSeconds(10), TimeProvider.System);
        var workspace = await client.Server.CreateApplicationWorkspaceAsync();
        var first = await workspace.BeginRevisionAsync();
        var declaration = await first.AddResourceAsync("service", "example/service");
        var original = await first.AddSecretParameterAsync("password", "original-secret");
        await declaration.SetSymbolicConfigurationAsync("password", original);
        await workspace.CommitRevisionAsync(first);
        var execution = await workspace.GetApplicationExecutionAsync();
        var owner = await client.Server.JoinResourceExecutionAsync(await execution.InviteResourceExecutionAsync(declaration));
        var observer = await client.Server.JoinApplicationObserverAsync(await execution.InviteApplicationObserverAsync());
        await owner.CompleteResourceConfigurationAsync(1, new RuntimeOperationResult { Status = "succeeded", Message = "original-secret" });
        Assert.Equal("[REDACTED]", (await owner.ReadResourceConfigurationAsync()).Message);
        var revision = await workspace.BeginRevisionAsync();
        var updated = await revision.AddResourceAsync("service", "example/service");
        await updated.SetSymbolicConfigurationAsync("password", await revision.AddSecretParameterAsync("password", "updated-secret"));
        await workspace.CommitRevisionAsync(revision);
        var configuration = await owner.ReadResourceConfigurationAsync();
        Assert.Equal(2, configuration.Revision);
        Assert.Equal("pending", configuration.Status);
        Assert.Empty(configuration.Properties);
        Assert.True(Assert.Single((await owner.InspectExecutionSymbolicConfigurationAsync()).Properties).IsSecret);
        var stale = await Assert.ThrowsAsync<NativeCapabilityException>(() => owner.StartContainerWithValuesAsync(1, "container",
            new ContainerWorkloadLaunch { Image = "redis:8", Entrypoint = "", Mounts = [], Environment = [], Arguments = [], Endpoints = [] },
            new SymbolicLaunchBindings { Environment = [new SymbolicLaunchEnvironment { Name = "PASSWORD", ConfigurationKey = "password" }], Arguments = [] }));
        Assert.Equal("VALUE_REVISION_STALE", stale.Code);
        var revoked = await Assert.ThrowsAsync<NativeCapabilityException>(() => original.EncodeUriValueAsync());
        Assert.Equal("HANDLE_NOT_FOUND", revoked.Code);
        await owner.AppendResourceLogAsync("stdout", "original-secret updated-secret");
        var resourceId = Assert.Single((await observer.ReadResourceObservationsAsync()).Resources).ResourceId;
        Assert.Equal("[REDACTED] [REDACTED]", Assert.Single((await observer.ReadResourceLogsAsync(resourceId, 0)).Entries).Message);
        await workspace.RetireApplicationWorkspaceAsync();
    }
 
    [Fact]
    public async Task SymbolicContainerEndpointReferencesHaveAnExplicitTypedUnsupportedFailure()
    {
        await using var host = await NativeClientTestHost.StartAsync();
        await using var client = await NativeClient.ConnectAsync(host.Endpoint, host.Token, TimeSpan.FromSeconds(10), TimeProvider.System);
        var session = await client.Server.OpenApplicationAsync();
        var composition = await session.StartGenerationAsync();
        var source = await composition.AddResourceAsync("source", "example/source");
        var target = await composition.AddResourceAsync("target", "example/target");
        await target.SetSymbolicConfigurationAsync("endpoint", await composition.CreateEndpointValueAsync(source, "tcp", "host"));
        var execution = await composition.CreateExecutionAsync();
        var owner = await client.Server.JoinResourceExecutionAsync(await execution.InviteResourceExecutionAsync(target));
        var error = await Assert.ThrowsAsync<NativeCapabilityException>(() => owner.StartContainerWithValuesAsync(1, "container",
            new ContainerWorkloadLaunch { Image = "redis:8", Entrypoint = "", Mounts = [], Environment = [], Arguments = [], Endpoints = [] },
            new SymbolicLaunchBindings { Environment = [new SymbolicLaunchEnvironment { Name = "ENDPOINT", ConfigurationKey = "endpoint" }], Arguments = [] }));
        Assert.Equal("VALUE_NETWORK_CONTEXT_UNSUPPORTED", error.Code);
        await session.RetireGenerationAsync(composition);
    }
}