| File: Utils\PathNormalizerTests.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 Aspire.Hosting.Utils; namespace Aspire.Cli.Tests.Utils; public class PathNormalizerTests(ITestOutputHelper outputHelper) { [Fact] public void ResolveSymlinks_IsIdempotent_WhenPathHasNoSymlinks() { // The input itself may sit under a symlinked root (for example /var -> /private/var // on macOS), so we cannot assert the result equals the input. We can assert // idempotence: a path with no remaining symlinks must resolve to itself. using var workspace = TemporaryWorkspace.CreateForCli(outputHelper); var subdir = workspace.WorkspaceRoot.CreateSubdirectory("App"); var file = new FileInfo(Path.Combine(subdir.FullName, "app.csproj")); File.WriteAllText(file.FullName, "<Project />"); var firstPass = PathNormalizer.ResolveSymlinks(file.FullName); var secondPass = PathNormalizer.ResolveSymlinks(firstPass); Assert.Equal(firstPass, secondPass); } [Fact] public void ResolveSymlinks_ReturnsInputUnchanged_WhenEmpty() { Assert.Equal(string.Empty, PathNormalizer.ResolveSymlinks(string.Empty)); } [Fact] public void ResolveSymlinks_ResolvesFinalFileSymlink() { using var workspace = TemporaryWorkspace.CreateForCli(outputHelper); var target = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "target.csproj")); File.WriteAllText(target.FullName, "<Project />"); var linkPath = Path.Combine(workspace.WorkspaceRoot.FullName, "link.csproj"); TryCreateSymlink(linkPath, target.FullName, isDirectory: false); var resolved = PathNormalizer.ResolveSymlinks(linkPath); // The link's final target should be canonical-equal to the real file. We use // ResolveSymlinks on the target as well to account for the temp directory itself // sitting under a symlinked root (for example /tmp -> /private/tmp on macOS). Assert.Equal(PathNormalizer.ResolveSymlinks(target.FullName), resolved); } [Fact] public void ResolveSymlinks_ResolvesIntermediateDirectorySymlink() { // The L5 repro relies on a symlink that is NOT the final segment: on macOS, // /tmp -> /private/tmp, and the apphost lives at /tmp/L5/x.cs. A single call to // Directory.ResolveLinkTarget on the full path would not unwrap /tmp, so the // implementation must walk segments. using var workspace = TemporaryWorkspace.CreateForCli(outputHelper); var realDirectory = workspace.WorkspaceRoot.CreateSubdirectory("real"); var nested = realDirectory.CreateSubdirectory("nested"); var file = new FileInfo(Path.Combine(nested.FullName, "app.csproj")); File.WriteAllText(file.FullName, "<Project />"); var linkDirectory = Path.Combine(workspace.WorkspaceRoot.FullName, "link"); TryCreateSymlink(linkDirectory, realDirectory.FullName, isDirectory: true); // Path through the link should resolve to the same canonical path as the path // through the real directory. var pathThroughLink = Path.Combine(linkDirectory, "nested", "app.csproj"); var resolvedThroughLink = PathNormalizer.ResolveSymlinks(pathThroughLink); var resolvedThroughReal = PathNormalizer.ResolveSymlinks(file.FullName); Assert.Equal(resolvedThroughReal, resolvedThroughLink); } [Fact] public void ResolveSymlinks_PreservesPath_WhenLinkIsBroken() { using var workspace = TemporaryWorkspace.CreateForCli(outputHelper); var missingTarget = Path.Combine(workspace.WorkspaceRoot.FullName, "missing.csproj"); var linkPath = Path.Combine(workspace.WorkspaceRoot.FullName, "broken-link.csproj"); TryCreateSymlink(linkPath, missingTarget, isDirectory: false); // A broken link should not throw — the method must fall back to returning the // path so callers can still surface a useful "file not found" error. var resolved = PathNormalizer.ResolveSymlinks(linkPath); Assert.False(string.IsNullOrEmpty(resolved)); } private static void TryCreateSymlink(string linkPath, string targetPath, bool isDirectory) { try { if (isDirectory) { Directory.CreateSymbolicLink(linkPath, targetPath); } else { File.CreateSymbolicLink(linkPath, targetPath); } } catch (UnauthorizedAccessException ex) { // Creating symlinks on Windows requires either administrator rights or // Developer Mode. Skip cleanly on environments that don't allow it rather // than failing the test for an environment reason. Assert.Skip($"Cannot create symbolic links in this environment: {ex.Message}"); } catch (IOException ex) { Assert.Skip($"Symbolic link creation failed in this environment: {ex.Message}"); } } }