| File: DotNet\DotNetAppHostUpdateTransactionTests.cs | Web Access |
| Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.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.Text; using Aspire.Cli.DotNet; using Aspire.Hosting.Utils; namespace Aspire.Cli.Tests.DotNet; public class DotNetAppHostUpdateTransactionTests(ITestOutputHelper outputHelper) { [Theory] [InlineData(false)] [InlineData(true)] public async Task PreparedApply_PreservesConcurrentConfigWriter(bool existing) { using var workspace = TemporaryWorkspace.CreateForCli(outputHelper); var file = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "NuGet.Config")); var original = existing ? Encoding.UTF8.GetBytes("<configuration />") : null; if (original is not null) { await File.WriteAllBytesAsync(file.FullName, original); } var candidate = new DotNetAppHostNuGetConfigMergerCandidate( file, original, Encoding.UTF8.GetBytes("<configuration><candidate /></configuration>")); await using (var transaction = new DotNetAppHostUpdateTransaction(new Dictionary<string, byte[]?> { [PathNormalizer.ResolveToFilesystemPath(file.FullName)] = original })) { await Assert.ThrowsAnyAsync<Exception>(() => transaction.ApplyPreparedAsync( file, candidate.ProposedContent, async writeStarting => { await File.WriteAllTextAsync(file.FullName, "<configuration><external /></configuration>"); await DotNetAppHostNuGetConfigMerger.ApplyAsync(candidate, writeStarting, TestContext.Current.CancellationToken); }, TestContext.Current.CancellationToken)); } Assert.Equal("<configuration><external /></configuration>", await File.ReadAllTextAsync(file.FullName)); } [Theory] [InlineData(false)] [InlineData(true)] public async Task PreparedApply_RollsBackSuccessfulOrPartialOwnedWrites(bool succeeds) { using var workspace = TemporaryWorkspace.CreateForCli(outputHelper); var file = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "NuGet.Config")); var original = Encoding.UTF8.GetBytes("<configuration />"); await File.WriteAllBytesAsync(file.FullName, original); var proposed = Encoding.UTF8.GetBytes("<configuration><candidate /></configuration>"); await using (var transaction = new DotNetAppHostUpdateTransaction(new Dictionary<string, byte[]?> { [PathNormalizer.ResolveToFilesystemPath(file.FullName)] = original })) { async Task WriteAsync(Action writeStarting) { writeStarting(); await File.WriteAllBytesAsync(file.FullName, succeeds ? proposed : proposed[..10]); if (!succeeds) { throw new IOException("Partial write."); } } if (succeeds) { await transaction.ApplyPreparedAsync(file, proposed, WriteAsync, TestContext.Current.CancellationToken); } else { await Assert.ThrowsAsync<IOException>(() => transaction.ApplyPreparedAsync(file, proposed, WriteAsync, TestContext.Current.CancellationToken)); } } Assert.Equal(original, await File.ReadAllBytesAsync(file.FullName)); } [Fact] public async Task Rollback_RefusesToOverwriteAnExternalEditOfStagedSource() { using var workspace = TemporaryWorkspace.CreateForCli(outputHelper); var file = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.cs")); var original = Encoding.UTF8.GetBytes("#:sdk Aspire.AppHost.Sdk@9.4.1"); await File.WriteAllBytesAsync(file.FullName, original); var transaction = new DotNetAppHostUpdateTransaction(new Dictionary<string, byte[]?> { [PathNormalizer.ResolveToFilesystemPath(file.FullName)] = original }); await transaction.ApplyAsync([file], () => File.WriteAllTextAsync(file.FullName, "#:sdk Aspire.AppHost.Sdk@9.4.2"), TestContext.Current.CancellationToken); await File.WriteAllTextAsync(file.FullName, "// External edit."); var failure = await Assert.ThrowsAsync<AggregateException>(() => transaction.DisposeAsync().AsTask()); Assert.Single(failure.InnerExceptions); Assert.Equal("// External edit.", await File.ReadAllTextAsync(file.FullName)); await transaction.DisposeAsync(); } [Fact] public async Task Verify_RejectsChangesBeforeAnyCandidateEdit() { using var workspace = TemporaryWorkspace.CreateForCli(outputHelper); var file = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.cs")); var original = Encoding.UTF8.GetBytes("#:sdk Aspire.AppHost.Sdk@9.4.1"); await File.WriteAllBytesAsync(file.FullName, original); await using var transaction = new DotNetAppHostUpdateTransaction(new Dictionary<string, byte[]?> { [PathNormalizer.ResolveToFilesystemPath(file.FullName)] = original }); await File.WriteAllTextAsync(file.FullName, "// External edit."); var invoked = false; await Assert.ThrowsAsync<InvalidOperationException>(() => transaction.ApplyAsync([file], () => { invoked = true; return Task.CompletedTask; }, TestContext.Current.CancellationToken)); Assert.False(invoked); Assert.Equal("// External edit.", await File.ReadAllTextAsync(file.FullName)); } }