diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d475752..ddbd46c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,11 @@ Use the following shell command: ```console $ dotnet test ``` -Tests involving symlink creation require elevated privileges on Windows, as this is enforced at the OS level. + +On Windows, tests that create symbolic links require either [Developer Mode][windows-developer-mode] or an elevated +process. Developer Mode lets an unelevated process create symbolic links, as documented for the +[`CreateSymbolicLinkW`][create-symbolic-link] API. When neither is available, these tests are skipped locally; CI +runners fail instead, ensuring that the tests are not silently skipped in automation. Test Documentation ------------------ @@ -73,6 +77,8 @@ Then run the following shell command: $ dotnet fsi scripts/github-actions.fsx ``` +[create-symbolic-link]: https://learn.microsoft.com/windows/win32/api/winbase/nf-winbase-createsymboliclinkw [dotnet-sdk]: https://dotnet.microsoft.com/en-us/download [powershell]: https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell [reuse]: https://reuse.software/ +[windows-developer-mode]: https://learn.microsoft.com/windows/apps/get-started/enable-your-device-for-development diff --git a/TruePath.Tests/AbsolutePathTests.cs b/TruePath.Tests/AbsolutePathTests.cs index 78c3e01..a9284b0 100644 --- a/TruePath.Tests/AbsolutePathTests.cs +++ b/TruePath.Tests/AbsolutePathTests.cs @@ -135,18 +135,7 @@ public void ReadKind_IsSymlink() var tempDirectoryInfo = Path.GetTempPath(); - try - { - Directory.CreateSymbolicLink(currentDirectory, tempDirectoryInfo); - } - catch (IOException e) when (OperatingSystem.IsWindows() && !Utils.RunsOnCi()) - { - if (e.Message.Contains("privilege") && (e.StackTrace?.Contains("CreateSymbolicLink") ?? false)) - { - Assert.Skip("Tests involving symlink creation require elevated privileges on Windows, as this is enforced at the OS level."); - } - else throw; - } + Utils.CreateSymbolicLinkOrSkip(() => Directory.CreateSymbolicLink(currentDirectory, tempDirectoryInfo)); // Act var kind = localPath.ReadKind(); diff --git a/TruePath.Tests/Utils.cs b/TruePath.Tests/Utils.cs index 70da5b6..a3db168 100644 --- a/TruePath.Tests/Utils.cs +++ b/TruePath.Tests/Utils.cs @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: MIT +using Windows.Win32; +using Windows.Win32.Foundation; using Xunit.v3; namespace TruePath.Tests; @@ -44,6 +46,24 @@ internal static bool RunsOnCi() return Environment.GetEnvironmentVariable("CI") is not null; } + internal static void CreateSymbolicLinkOrSkip(Action createSymbolicLink) + { + try + { + createSymbolicLink(); + } + catch (IOException e) when (OperatingSystem.IsWindows() + && e.HResult == PInvoke.HRESULT_FROM_WIN32(WIN32_ERROR.ERROR_PRIVILEGE_NOT_HELD).Value) + { + Assert.False( + RunsOnCi(), + "CI runners must allow symbolic link creation so these tests are not skipped silently."); + Assert.Skip( + "Creating symbolic links on Windows requires Developer Mode or an elevated process. " + + "See CONTRIBUTING.md for setup instructions."); + } + } + /// /// Changes the current directory to , and restores the previous one on disposal. /// diff --git a/TruePath/NativeMethods.txt b/TruePath/NativeMethods.txt index f638895..591618f 100644 --- a/TruePath/NativeMethods.txt +++ b/TruePath/NativeMethods.txt @@ -7,4 +7,6 @@ FILE_ACCESS_RIGHTS FILE_ATTRIBUTE_TAG_INFO GetFileInformationByHandleEx GetFinalPathNameByHandle +HRESULT_FROM_WIN32 IO_REPARSE_TAG_MOUNT_POINT +WIN32_ERROR