From 5b04bce6567855325810bc4e6bcd2f6e05b329c7 Mon Sep 17 00:00:00 2001 From: Sean Hall Date: Thu, 28 Apr 2022 21:02:23 -0500 Subject: Port UtilExtension.UserTests from wix3. --- src/test/burn/WixTestTools/UserVerifier.cs | 348 +++++++++++++++++++++ src/test/burn/WixTestTools/WixTestTools.csproj | 2 + src/test/msi/Directory.Build.props | 11 + src/test/msi/Directory.Build.targets | 6 + src/test/msi/Directory.wixproj.props | 11 + src/test/msi/Directory.wixproj.targets | 13 + src/test/msi/MsiE2ETests.sln | 51 +++ src/test/msi/README.md | 29 ++ .../msi/TestData/Templates/CustomActionFail.exe | Bin 0 -> 4608 bytes src/test/msi/TestData/Templates/Product.wxs | 47 +++ src/test/msi/TestData/TestData.proj | 16 + .../ProductA/ProductA.wixproj | 13 + .../UtilExtensionUserTests/ProductA/product.wxs | 35 +++ .../ProductFail/ProductFail.wixproj | 13 + .../ProductFail/product_fail.wxs | 39 +++ .../ProductFailIfExists/FailIfExists.wxs | 24 ++ .../ProductFailIfExists.wixproj | 13 + .../ProductNonVitalUserGroup/NonVitalUserGroup.wxs | 22 ++ .../ProductNonVitalUserGroup.wixproj | 13 + .../ProductRestrictedDomain.wixproj | 13 + .../ProductRestrictedDomain/RestrictedDomain.wxs | 20 ++ .../msi/WixToolsetTest.MsiE2E/MsiE2EFixture.cs | 28 ++ src/test/msi/WixToolsetTest.MsiE2E/MsiE2ETests.cs | 44 +++ .../UtilExtensionUserTests.cs | 162 ++++++++++ .../WixToolsetTest.MsiE2E.csproj | 31 ++ src/test/msi/WixToolsetTest.MsiE2E/runtests.cmd | 2 + src/test/msi/test_msi.cmd | 23 ++ src/test/test.cmd | 1 + 28 files changed, 1030 insertions(+) create mode 100644 src/test/burn/WixTestTools/UserVerifier.cs create mode 100644 src/test/msi/Directory.Build.props create mode 100644 src/test/msi/Directory.Build.targets create mode 100644 src/test/msi/Directory.wixproj.props create mode 100644 src/test/msi/Directory.wixproj.targets create mode 100644 src/test/msi/MsiE2ETests.sln create mode 100644 src/test/msi/README.md create mode 100644 src/test/msi/TestData/Templates/CustomActionFail.exe create mode 100644 src/test/msi/TestData/Templates/Product.wxs create mode 100644 src/test/msi/TestData/TestData.proj create mode 100644 src/test/msi/TestData/UtilExtensionUserTests/ProductA/ProductA.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionUserTests/ProductA/product.wxs create mode 100644 src/test/msi/TestData/UtilExtensionUserTests/ProductFail/ProductFail.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionUserTests/ProductFail/product_fail.wxs create mode 100644 src/test/msi/TestData/UtilExtensionUserTests/ProductFailIfExists/FailIfExists.wxs create mode 100644 src/test/msi/TestData/UtilExtensionUserTests/ProductFailIfExists/ProductFailIfExists.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionUserTests/ProductNonVitalUserGroup/NonVitalUserGroup.wxs create mode 100644 src/test/msi/TestData/UtilExtensionUserTests/ProductNonVitalUserGroup/ProductNonVitalUserGroup.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionUserTests/ProductRestrictedDomain/ProductRestrictedDomain.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionUserTests/ProductRestrictedDomain/RestrictedDomain.wxs create mode 100644 src/test/msi/WixToolsetTest.MsiE2E/MsiE2EFixture.cs create mode 100644 src/test/msi/WixToolsetTest.MsiE2E/MsiE2ETests.cs create mode 100644 src/test/msi/WixToolsetTest.MsiE2E/UtilExtensionUserTests.cs create mode 100644 src/test/msi/WixToolsetTest.MsiE2E/WixToolsetTest.MsiE2E.csproj create mode 100644 src/test/msi/WixToolsetTest.MsiE2E/runtests.cmd create mode 100644 src/test/msi/test_msi.cmd (limited to 'src/test') diff --git a/src/test/burn/WixTestTools/UserVerifier.cs b/src/test/burn/WixTestTools/UserVerifier.cs new file mode 100644 index 00000000..b5218a79 --- /dev/null +++ b/src/test/burn/WixTestTools/UserVerifier.cs @@ -0,0 +1,348 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixTestTools +{ + using System; + using System.Text; + using System.DirectoryServices; + using System.DirectoryServices.AccountManagement; + using System.Security.Principal; + using Xunit; + + /// + /// Contains methods for User account verification + /// + public static class UserVerifier + { + public static class SIDStrings + { + // Built-In Local Groups + public static readonly string BUILTIN_ADMINISTRATORS = "S-1-5-32-544"; + public static readonly string BUILTIN_USERS = "S-1-5-32-545"; + public static readonly string BUILTIN_GUESTS = "S-1-5-32-546"; + public static readonly string BUILTIN_ACCOUNT_OPERATORS = "S-1-5-32-548"; + public static readonly string BUILTIN_SERVER_OPERATORS = "S-1-5-32-549"; + public static readonly string BUILTIN_PRINT_OPERATORS = "S-1-5-32-550"; + public static readonly string BUILTIN_BACKUP_OPERATORS = "S-1-5-32-551"; + public static readonly string BUILTIN_REPLICATOR = "S-1-5-32-552"; + + // Special Groups + public static readonly string CREATOR_OWNER = "S-1-3-0"; + public static readonly string EVERYONE = "S-1-1-0"; + public static readonly string NT_AUTHORITY_NETWORK = "S-1-5-2"; + public static readonly string NT_AUTHORITY_INTERACTIVE = "S-1-5-4"; + public static readonly string NT_AUTHORITY_SYSTEM = "S-1-5-18"; + public static readonly string NT_AUTHORITY_Authenticated_Users = "S-1-5-11"; + public static readonly string NT_AUTHORITY_LOCAL_SERVICE = "S-1-5-19"; + public static readonly string NT_AUTHORITY_NETWORK_SERVICE = "S-1-5-20"; + } + + /// + /// Create a local user on the machine + /// + /// + /// + /// Has to be run as an Admin + public static void CreateLocalUser(string userName, string password) + { + DeleteLocalUser(userName); + UserPrincipal newUser = new UserPrincipal(new PrincipalContext(ContextType.Machine)); + newUser.SetPassword(password); + newUser.Name = userName; + newUser.Description = "New test User"; + newUser.UserCannotChangePassword = true; + newUser.PasswordNeverExpires = false; + newUser.Save(); + } + + /// + /// Deletes a local user from the machine + /// + /// user name to delete + /// Has to be run as an Admin + public static void DeleteLocalUser(string userName) + { + UserPrincipal newUser = GetUser(String.Empty, userName); + if (null != newUser) + { + newUser.Delete(); + } + } + + /// + /// Verifies that a user exisits or not + /// + /// domain name for the user, empty for local users + /// the user name + public static bool UserExists(string domainName, string userName) + { + UserPrincipal user = GetUser(domainName, userName); + + return null != user; + } + + /// + /// Sets the user information for a given user + /// + /// domain name for the user, empty for local users + /// the user name + /// user is required to change the password on first login + /// password never expires + /// account is disabled + public static void SetUserInformation(string domainName, string userName, bool passwordExpired, bool passwordNeverExpires, bool disabled) + { + UserPrincipal user = GetUser(domainName, userName); + + Assert.False(null == user, String.Format("User '{0}' was not found under domain '{1}'.", userName, domainName)); + user.PasswordNeverExpires = passwordNeverExpires; + user.Enabled = !disabled; + if (passwordExpired) + { + user.ExpirePasswordNow(); + } + else + { + // extend the expiration date to a month + user.AccountExpirationDate = DateTime.Now.Add(new TimeSpan(30, 0, 0, 0, 0)); + } + user.Save(); + } + + /// + /// Adds the specified user to the specified local group + /// + /// User to add + /// Group to add too + public static void AddUserToGroup(string userName, string groupName) + { + DirectoryEntry localMachine; + DirectoryEntry localGroup; + + localMachine = new DirectoryEntry("WinNT://" + Environment.MachineName.ToString()); + localGroup = localMachine.Children.Find(groupName, "group"); + Assert.False(null == localGroup, String.Format("Group '{0}' was not found.", groupName)); + DirectoryEntry user = FindActiveDirectoryUser(userName); + localGroup.Invoke("Add", new object[] { user.Path.ToString() }); + } + + /// + /// Find the specified user in AD + /// + /// user name to lookup + /// DirectoryEntry of the user + private static DirectoryEntry FindActiveDirectoryUser(string UserName) + { + var mLocalMachine = new DirectoryEntry("WinNT://" + Environment.MachineName.ToString()); + var mLocalEntries = mLocalMachine.Children; + + var theUser = mLocalEntries.Find(UserName); + return theUser; + } + + /// + /// Verifies the user information for a given user + /// + /// domain name for the user, empty for local users + /// the user name + /// user is required to change the password on first login + /// password never expires + /// account is disabled + public static void VerifyUserInformation(string domainName, string userName, bool passwordExpired, bool passwordNeverExpires, bool disabled) + { + UserPrincipal user = GetUser(domainName, userName); + + Assert.False(null == user, String.Format("User '{0}' was not found under domain '{1}'.", userName, domainName)); + + Assert.True(passwordNeverExpires == user.PasswordNeverExpires, String.Format("Password Never Expires for user '{0}/{1}' is: '{2}', expected: '{3}'.", domainName, userName, user.PasswordNeverExpires, passwordNeverExpires)); + Assert.True(disabled != user.Enabled, String.Format("Disappled for user '{0}/{1}' is: '{2}', expected: '{3}'.", domainName, userName, !user.Enabled, disabled)); + + DateTime expirationDate = user.AccountExpirationDate.GetValueOrDefault(); + bool accountExpired = expirationDate.ToLocalTime().CompareTo(DateTime.Now) <= 0; + Assert.True(passwordExpired == accountExpired, String.Format("Password Expired for user '{0}/{1}' is: '{2}', expected: '{3}'.", domainName, userName, accountExpired, passwordExpired)); + } + + /// + /// Verify that a givin user is member of a local group + /// + /// domain name for the user, empty for local users + /// the user name + /// list of groups to check for membership + public static void VerifyUserIsMemberOf(string domainName, string userName, params string[] groupNames) + { + IsUserMemberOf(domainName, userName, true, groupNames); + } + + /// + /// Verify that a givin user is NOT member of a local group + /// + /// domain name for the user, empty for local users + /// the user name + /// list of groups to check for membership + public static void VerifyUserIsNotMemberOf(string domainName, string userName, params string[] groupNames) + { + IsUserMemberOf(domainName, userName, false, groupNames); + } + + /// + /// + /// + /// SID to search for + /// AccountName + public static string GetLocalUserNameFromSID(string sidString) + { + SecurityIdentifier sid = new SecurityIdentifier(sidString); + NTAccount account = (NTAccount)sid.Translate(typeof(NTAccount)); + return account.Value; + } + + /// + /// Get the SID string for a given user name + /// + /// + /// + /// SID string + public static string GetSIDFromUserName(string Domain, string UserName) + { + string retVal = null; + string domain = Domain; + string name = UserName; + + if (String.IsNullOrEmpty(domain)) + { + domain = System.Environment.MachineName; + } + + try + { + DirectoryEntry de = new DirectoryEntry("WinNT://" + domain + "/" + name); + + long iBigVal = 5; + byte[] bigArr = BitConverter.GetBytes(iBigVal); + System.DirectoryServices.PropertyCollection coll = de.Properties; + object obVal = coll["objectSid"].Value; + if (null != obVal) + { + retVal = ConvertByteToSidString((byte[])obVal); + } + } + catch (Exception ex) + { + retVal = String.Empty; + Console.Write(ex.Message); + } + + return retVal; + } + + /// + /// converts a byte array containing a SID into a string + /// + /// + /// SID string + private static string ConvertByteToSidString(byte[] sidBytes) + { + short sSubAuthorityCount; + StringBuilder strSid = new StringBuilder(); + strSid.Append("S-"); + try + { + // Add SID revision. + strSid.Append(sidBytes[0].ToString()); + + sSubAuthorityCount = Convert.ToInt16(sidBytes[1]); + + // Next six bytes are SID authority value. + if (sidBytes[2] != 0 || sidBytes[3] != 0) + { + string strAuth = String.Format("0x{0:2x}{1:2x}{2:2x}{3:2x}{4:2x}{5:2x}", + (short)sidBytes[2], + (short)sidBytes[3], + (short)sidBytes[4], + (short)sidBytes[5], + (short)sidBytes[6], + (short)sidBytes[7]); + strSid.Append("-"); + strSid.Append(strAuth); + } + else + { + long iVal = (int)(sidBytes[7]) + + (int)(sidBytes[6] << 8) + + (int)(sidBytes[5] << 16) + + (int)(sidBytes[4] << 24); + strSid.Append("-"); + strSid.Append(iVal.ToString()); + } + + // Get sub authority count... + int idxAuth = 0; + for (int i = 0; i < sSubAuthorityCount; i++) + { + idxAuth = 8 + i * 4; + uint iSubAuth = BitConverter.ToUInt32(sidBytes, idxAuth); + strSid.Append("-"); + strSid.Append(iSubAuth.ToString()); + } + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return ""; + } + return strSid.ToString(); + } + + /// + /// Verify that a given user is member of a local group + /// + /// domain name for the user, empty for local users + /// the user name + /// whether the user is expected to be a member of the groups or not + /// list of groups to check for membership + private static void IsUserMemberOf(string domainName, string userName, bool shouldBeMember, params string[] groupNames) + { + UserPrincipal user = GetUser(domainName, userName); + Assert.False(null == user, String.Format("User '{0}' was not found under domain '{1}'.", userName, domainName)); + + bool missedAGroup = false; + string message = String.Empty; + foreach (string groupName in groupNames) + { + try + { + bool found = user.IsMemberOf(new PrincipalContext(ContextType.Machine), IdentityType.Name, groupName); + if (found != shouldBeMember) + { + missedAGroup = true; + message += String.Format("User '{0}/{1}' is {2} a member of local group '{3}'. \r\n", domainName, userName, found ? String.Empty : "NOT", groupName); + } + } + catch (System.DirectoryServices.AccountManagement.PrincipalOperationException) + { + missedAGroup = true; + message += String.Format("Local group '{0}' was not found. \r\n", groupName); + } + + } + Assert.False(missedAGroup, message); + } + + /// + /// Returns the UserPrincipal object for a given user + /// + /// Domain name to look under, if Empty the LocalMachine is assumned as the domain + /// + /// UserPrinicipal Object for the user if found, or null other wise + private static UserPrincipal GetUser(string domainName, string userName) + { + if (String.IsNullOrEmpty(domainName)) + { + return UserPrincipal.FindByIdentity(new PrincipalContext(ContextType.Machine), IdentityType.Name, userName); + } + else + { + return UserPrincipal.Current;//.FindByIdentity(new PrincipalContext(ContextType.Domain,domainName), IdentityType.Name, userName); + } + } + } +} diff --git a/src/test/burn/WixTestTools/WixTestTools.csproj b/src/test/burn/WixTestTools/WixTestTools.csproj index 7b726560..49e14f25 100644 --- a/src/test/burn/WixTestTools/WixTestTools.csproj +++ b/src/test/burn/WixTestTools/WixTestTools.csproj @@ -9,6 +9,8 @@ + + diff --git a/src/test/msi/Directory.Build.props b/src/test/msi/Directory.Build.props new file mode 100644 index 00000000..9e2a776f --- /dev/null +++ b/src/test/msi/Directory.Build.props @@ -0,0 +1,11 @@ + + + + + IntegrationMsi + false + + + + + diff --git a/src/test/msi/Directory.Build.targets b/src/test/msi/Directory.Build.targets new file mode 100644 index 00000000..4e97b6ca --- /dev/null +++ b/src/test/msi/Directory.Build.targets @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/test/msi/Directory.wixproj.props b/src/test/msi/Directory.wixproj.props new file mode 100644 index 00000000..f824ff2a --- /dev/null +++ b/src/test/msi/Directory.wixproj.props @@ -0,0 +1,11 @@ + + + + $([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName($(MSBuildProjectDirectory))))) + $(BaseOutputPath)obj\$(TestGroupName)\$(ProjectName)\ + $(OutputPath)netcoreapp3.1\TestData\$(TestGroupName)\ + None + -wx + true + + diff --git a/src/test/msi/Directory.wixproj.targets b/src/test/msi/Directory.wixproj.targets new file mode 100644 index 00000000..7dddb141 --- /dev/null +++ b/src/test/msi/Directory.wixproj.targets @@ -0,0 +1,13 @@ + + + + + $(MSBuildProjectName) + TestGroupName=$(TestGroupName);PackageName=$(PackageName);$(DefineConstants) + CabPrefix=$(CabPrefix);$(DefineConstants) + ProductCode=$(ProductCode);$(DefineConstants) + ProductComponents=1;$(DefineConstants) + UpgradeCode=$(UpgradeCode);$(DefineConstants) + Version=$(Version);$(DefineConstants) + + diff --git a/src/test/msi/MsiE2ETests.sln b/src/test/msi/MsiE2ETests.sln new file mode 100644 index 00000000..c8ba93ec --- /dev/null +++ b/src/test/msi/MsiE2ETests.sln @@ -0,0 +1,51 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31919.166 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixTestTools", "..\burn\WixTestTools\WixTestTools.csproj", "{3D3B02F3-79B6-4BD5-AD49-2889DA3849A7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixToolsetTest.MsiE2E", "WixToolsetTest.MsiE2E\WixToolsetTest.MsiE2E.csproj", "{68E1A5F8-0F44-4B38-8876-C101A2A019F2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3D3B02F3-79B6-4BD5-AD49-2889DA3849A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D3B02F3-79B6-4BD5-AD49-2889DA3849A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D3B02F3-79B6-4BD5-AD49-2889DA3849A7}.Debug|x64.ActiveCfg = Debug|Any CPU + {3D3B02F3-79B6-4BD5-AD49-2889DA3849A7}.Debug|x64.Build.0 = Debug|Any CPU + {3D3B02F3-79B6-4BD5-AD49-2889DA3849A7}.Debug|x86.ActiveCfg = Debug|Any CPU + {3D3B02F3-79B6-4BD5-AD49-2889DA3849A7}.Debug|x86.Build.0 = Debug|Any CPU + {3D3B02F3-79B6-4BD5-AD49-2889DA3849A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D3B02F3-79B6-4BD5-AD49-2889DA3849A7}.Release|Any CPU.Build.0 = Release|Any CPU + {3D3B02F3-79B6-4BD5-AD49-2889DA3849A7}.Release|x64.ActiveCfg = Release|Any CPU + {3D3B02F3-79B6-4BD5-AD49-2889DA3849A7}.Release|x64.Build.0 = Release|Any CPU + {3D3B02F3-79B6-4BD5-AD49-2889DA3849A7}.Release|x86.ActiveCfg = Release|Any CPU + {3D3B02F3-79B6-4BD5-AD49-2889DA3849A7}.Release|x86.Build.0 = Release|Any CPU + {68E1A5F8-0F44-4B38-8876-C101A2A019F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68E1A5F8-0F44-4B38-8876-C101A2A019F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68E1A5F8-0F44-4B38-8876-C101A2A019F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {68E1A5F8-0F44-4B38-8876-C101A2A019F2}.Debug|x64.Build.0 = Debug|Any CPU + {68E1A5F8-0F44-4B38-8876-C101A2A019F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {68E1A5F8-0F44-4B38-8876-C101A2A019F2}.Debug|x86.Build.0 = Debug|Any CPU + {68E1A5F8-0F44-4B38-8876-C101A2A019F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68E1A5F8-0F44-4B38-8876-C101A2A019F2}.Release|Any CPU.Build.0 = Release|Any CPU + {68E1A5F8-0F44-4B38-8876-C101A2A019F2}.Release|x64.ActiveCfg = Release|Any CPU + {68E1A5F8-0F44-4B38-8876-C101A2A019F2}.Release|x64.Build.0 = Release|Any CPU + {68E1A5F8-0F44-4B38-8876-C101A2A019F2}.Release|x86.ActiveCfg = Release|Any CPU + {68E1A5F8-0F44-4B38-8876-C101A2A019F2}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4C9978E3-8548-451E-8BF5-03AD1D0280C2} + EndGlobalSection +EndGlobal diff --git a/src/test/msi/README.md b/src/test/msi/README.md new file mode 100644 index 00000000..2b4ff9a4 --- /dev/null +++ b/src/test/msi/README.md @@ -0,0 +1,29 @@ +# integration + +This layer is for building installers, and then executing xunit tests that run them and verify that they worked. + +## Running tests + +The main focus of these tests is to validate behavior in a real environment. +Depending on who you talk to, these are integration or system-level or end-to-end (E2E) tests. +They modify machine state so it's strongly recommended *not* to run these tests on your dev box. +They should be run on a VM instead, where you can easily roll back. + +1. Run build.cmd to build everything (the tests will not automatically run). +1. Copy the build\IntegrationMsi\Debug\netcoreapp3.1 folder to your VM. +1. Open an elevated command prompt and navigate to the netcoreapp3.1 folder. +1. Run the runtests.cmd file to run the tests. + +You can modify the runtests.cmd to run specific tests. +For example, the following line runs only the specified test: + +> dotnet test --filter WixToolsetTest.BurnE2E.BasicFunctionalityTests.CanInstallAndUninstallSimpleBundle_x86_wixstdba WixToolsetTest.BurnE2E.dll + +The VM must have: +1. x64 .NET Core SDK of 3.1 or later (for the test runner) + +## Building with local changes + +The current build process will poison your NuGet package cache, so you may have to run the following command to clear it: + +> nuget locals all -clear \ No newline at end of file diff --git a/src/test/msi/TestData/Templates/CustomActionFail.exe b/src/test/msi/TestData/Templates/CustomActionFail.exe new file mode 100644 index 00000000..cceffe91 Binary files /dev/null and b/src/test/msi/TestData/Templates/CustomActionFail.exe differ diff --git a/src/test/msi/TestData/Templates/Product.wxs b/src/test/msi/TestData/Templates/Product.wxs new file mode 100644 index 00000000..c7604c1b --- /dev/null +++ b/src/test/msi/TestData/Templates/Product.wxs @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/TestData.proj b/src/test/msi/TestData/TestData.proj new file mode 100644 index 00000000..9e130987 --- /dev/null +++ b/src/test/msi/TestData/TestData.proj @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionUserTests/ProductA/ProductA.wixproj b/src/test/msi/TestData/UtilExtensionUserTests/ProductA/ProductA.wixproj new file mode 100644 index 00000000..fbc6f292 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionUserTests/ProductA/ProductA.wixproj @@ -0,0 +1,13 @@ + + + + {1A1795A6-87C0-4A9A-ABD5-DF9BED697037} + true + + + + + + + + \ No newline at end of file diff --git a/src/test/msi/TestData/UtilExtensionUserTests/ProductA/product.wxs b/src/test/msi/TestData/UtilExtensionUserTests/ProductA/product.wxs new file mode 100644 index 00000000..a7bec54e --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionUserTests/ProductA/product.wxs @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionUserTests/ProductFail/ProductFail.wixproj b/src/test/msi/TestData/UtilExtensionUserTests/ProductFail/ProductFail.wixproj new file mode 100644 index 00000000..e2fe3aa8 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionUserTests/ProductFail/ProductFail.wixproj @@ -0,0 +1,13 @@ + + + + {91D27DAC-04C1-4160-914E-343676D36CAA} + true + + + + + + + + \ No newline at end of file diff --git a/src/test/msi/TestData/UtilExtensionUserTests/ProductFail/product_fail.wxs b/src/test/msi/TestData/UtilExtensionUserTests/ProductFail/product_fail.wxs new file mode 100644 index 00000000..c5da862c --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionUserTests/ProductFail/product_fail.wxs @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionUserTests/ProductFailIfExists/FailIfExists.wxs b/src/test/msi/TestData/UtilExtensionUserTests/ProductFailIfExists/FailIfExists.wxs new file mode 100644 index 00000000..0da4f2b9 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionUserTests/ProductFailIfExists/FailIfExists.wxs @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionUserTests/ProductFailIfExists/ProductFailIfExists.wixproj b/src/test/msi/TestData/UtilExtensionUserTests/ProductFailIfExists/ProductFailIfExists.wixproj new file mode 100644 index 00000000..9e1a836f --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionUserTests/ProductFailIfExists/ProductFailIfExists.wixproj @@ -0,0 +1,13 @@ + + + + {BC803822-929E-47DA-AB3A-3A62EEEA2BFB} + true + + + + + + + + \ No newline at end of file diff --git a/src/test/msi/TestData/UtilExtensionUserTests/ProductNonVitalUserGroup/NonVitalUserGroup.wxs b/src/test/msi/TestData/UtilExtensionUserTests/ProductNonVitalUserGroup/NonVitalUserGroup.wxs new file mode 100644 index 00000000..461648ee --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionUserTests/ProductNonVitalUserGroup/NonVitalUserGroup.wxs @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionUserTests/ProductNonVitalUserGroup/ProductNonVitalUserGroup.wixproj b/src/test/msi/TestData/UtilExtensionUserTests/ProductNonVitalUserGroup/ProductNonVitalUserGroup.wixproj new file mode 100644 index 00000000..8734224d --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionUserTests/ProductNonVitalUserGroup/ProductNonVitalUserGroup.wixproj @@ -0,0 +1,13 @@ + + + + {455C8D4F-6D59-405C-AD51-0ACC7FB91A26} + true + + + + + + + + \ No newline at end of file diff --git a/src/test/msi/TestData/UtilExtensionUserTests/ProductRestrictedDomain/ProductRestrictedDomain.wixproj b/src/test/msi/TestData/UtilExtensionUserTests/ProductRestrictedDomain/ProductRestrictedDomain.wixproj new file mode 100644 index 00000000..e4a01a3a --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionUserTests/ProductRestrictedDomain/ProductRestrictedDomain.wixproj @@ -0,0 +1,13 @@ + + + + {50CF526C-A862-4327-9EA3-C96AAB6FABCE} + true + + + + + + + + \ No newline at end of file diff --git a/src/test/msi/TestData/UtilExtensionUserTests/ProductRestrictedDomain/RestrictedDomain.wxs b/src/test/msi/TestData/UtilExtensionUserTests/ProductRestrictedDomain/RestrictedDomain.wxs new file mode 100644 index 00000000..f200a06a --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionUserTests/ProductRestrictedDomain/RestrictedDomain.wxs @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/WixToolsetTest.MsiE2E/MsiE2EFixture.cs b/src/test/msi/WixToolsetTest.MsiE2E/MsiE2EFixture.cs new file mode 100644 index 00000000..a7646568 --- /dev/null +++ b/src/test/msi/WixToolsetTest.MsiE2E/MsiE2EFixture.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.MsiE2E +{ + using System; + using System.Security.Principal; + + public class MsiE2EFixture + { + const string RequiredEnvironmentVariableName = "RuntimeTestsEnabled"; + + public MsiE2EFixture() + { + using var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + if (!principal.IsInRole(WindowsBuiltInRole.Administrator)) + { + throw new InvalidOperationException("These tests must run elevated."); + } + + var testsEnabledString = Environment.GetEnvironmentVariable(RequiredEnvironmentVariableName); + if (!bool.TryParse(testsEnabledString, out var testsEnabled) || !testsEnabled) + { + throw new InvalidOperationException($"These tests affect machine state. Set the {RequiredEnvironmentVariableName} environment variable to true to accept the consequences."); + } + } + } +} diff --git a/src/test/msi/WixToolsetTest.MsiE2E/MsiE2ETests.cs b/src/test/msi/WixToolsetTest.MsiE2E/MsiE2ETests.cs new file mode 100644 index 00000000..22f2173b --- /dev/null +++ b/src/test/msi/WixToolsetTest.MsiE2E/MsiE2ETests.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.MsiE2E +{ + using System; + using System.Collections.Generic; + using WixTestTools; + using Xunit; + using Xunit.Abstractions; + + [Collection("MsiE2E")] + public abstract class MsiE2ETests : WixTestBase, IDisposable + { + protected MsiE2ETests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + } + + private Stack Installers { get; } = new Stack(); + + protected PackageInstaller CreatePackageInstaller(string filename) + { + var installer = new PackageInstaller(this.TestContext, filename); + this.Installers.Push(installer); + return installer; + } + + public void Dispose() + { + while (this.Installers.TryPop(out var installer)) + { + try + { + installer.Dispose(); + } + catch { } + } + } + } + + [CollectionDefinition("MsiE2E", DisableParallelization = true)] + public class MsiE2ECollectionDefinition : ICollectionFixture + { + } +} diff --git a/src/test/msi/WixToolsetTest.MsiE2E/UtilExtensionUserTests.cs b/src/test/msi/WixToolsetTest.MsiE2E/UtilExtensionUserTests.cs new file mode 100644 index 00000000..21491858 --- /dev/null +++ b/src/test/msi/WixToolsetTest.MsiE2E/UtilExtensionUserTests.cs @@ -0,0 +1,162 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.MsiE2E +{ + using System; + using WixTestTools; + using Xunit; + using Xunit.Abstractions; + + public class UtilExtensionUserTests : MsiE2ETests + { + public UtilExtensionUserTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } + + const string TempDomain = "USERDOMAIN"; + const string TempUsername = "USERNAME"; + + // Verify that the users specified in the authoring are created as expected. + [Fact] + public void CanInstallAndUninstallUsers() + { + var arguments = new string[] + { + $"TEMPDOMAIN={Environment.GetEnvironmentVariable(TempDomain)}", + $"TEMPUSERNAME={Environment.GetEnvironmentVariable(TempUsername)}", + }; + var productA = this.CreatePackageInstaller("ProductA"); + + productA.InstallProduct(MSIExec.MSIExecReturnCode.SUCCESS, arguments); + + // Validate New User Information. + UserVerifier.VerifyUserInformation(String.Empty, "testName1", true, false, false); + UserVerifier.VerifyUserIsMemberOf(String.Empty, "testName1", "Administrators", "Power Users"); + + UserVerifier.VerifyUserInformation(String.Empty, "testName2", true, true, true); + UserVerifier.VerifyUserIsMemberOf(String.Empty, "testName2", "Power Users"); + + UserVerifier.VerifyUserIsMemberOf(Environment.GetEnvironmentVariable(TempDomain), Environment.GetEnvironmentVariable(TempUsername), "Power Users"); + + productA.UninstallProduct(MSIExec.MSIExecReturnCode.SUCCESS, arguments); + + // Verify Users marked as RemoveOnUninstall were removed. + Assert.False(UserVerifier.UserExists(String.Empty, "testName1"), String.Format("User '{0}' was not removed on Uninstall", "testName1")); + Assert.True(UserVerifier.UserExists(String.Empty, "testName2"), String.Format("User '{0}' was removed on Uninstall", "testName2")); + + // clean up + UserVerifier.DeleteLocalUser("testName2"); + + UserVerifier.VerifyUserIsNotMemberOf(Environment.GetEnvironmentVariable(TempDomain), Environment.GetEnvironmentVariable(TempUsername), "Power Users"); + } + + // Verify the rollback action reverts all Users changes. + [Fact] + public void CanRollbackUsers() + { + var arguments = new string[] + { + $"TEMPDOMAIN={Environment.GetEnvironmentVariable(TempDomain)}", + $"TEMPUSERNAME={Environment.GetEnvironmentVariable(TempUsername)}", + }; + var productFail = this.CreatePackageInstaller("ProductFail"); + + // make sure the user accounts are deleted before we start + UserVerifier.DeleteLocalUser("testName1"); + UserVerifier.DeleteLocalUser("testName2"); + UserVerifier.VerifyUserIsNotMemberOf(Environment.GetEnvironmentVariable(TempDomain), Environment.GetEnvironmentVariable(TempUsername), "Power Users"); + + productFail.InstallProduct(MSIExec.MSIExecReturnCode.ERROR_INSTALL_FAILURE, arguments); + + // Verify Users marked as RemoveOnUninstall were removed. + Assert.False(UserVerifier.UserExists(String.Empty, "testName1"), String.Format("User '{0}' was not removed on Rollback", "testName1")); + Assert.False(UserVerifier.UserExists(String.Empty, "testName2"), String.Format("User '{0}' was not removed on Rollback", "testName2")); + + UserVerifier.VerifyUserIsNotMemberOf(Environment.GetEnvironmentVariable(TempDomain), Environment.GetEnvironmentVariable(TempUsername), "Power Users"); + } + + // Verify that the users specified in the authoring are created as expected on repair. + [Fact(Skip = "Test demonstrates failure")] + public void CanRepairUsers() + { + var arguments = new string[] + { + $"TEMPDOMAIN={Environment.GetEnvironmentVariable(TempDomain)}", + $"TEMPUSERNAME={Environment.GetEnvironmentVariable(TempUsername)}", + }; + var productA = this.CreatePackageInstaller("ProductA"); + + productA.InstallProduct(MSIExec.MSIExecReturnCode.SUCCESS, arguments); + + UserVerifier.DeleteLocalUser("testName1"); + UserVerifier.SetUserInformation(String.Empty, "testName2", true, false, false); + + productA.RepairProduct(MSIExec.MSIExecReturnCode.SUCCESS, arguments); + + // Validate New User Information. + UserVerifier.VerifyUserInformation(String.Empty, "testName1", true, false, false); + UserVerifier.VerifyUserIsMemberOf(String.Empty, "testName1", "Administrators", "Power Users"); + + UserVerifier.VerifyUserInformation(String.Empty, "testName2", true, true, true); + UserVerifier.VerifyUserIsMemberOf(String.Empty, "testName2", "Power Users"); + + UserVerifier.VerifyUserIsMemberOf(Environment.GetEnvironmentVariable(TempDomain), Environment.GetEnvironmentVariable(TempUsername), "Power Users"); + + productA.UninstallProduct(MSIExec.MSIExecReturnCode.SUCCESS, arguments); + + // Verify Users marked as RemoveOnUninstall were removed. + Assert.False(UserVerifier.UserExists(String.Empty, "testName1"), String.Format("User '{0}' was not removed on Uninstall", "testName1")); + Assert.True(UserVerifier.UserExists(String.Empty, "testName2"), String.Format("User '{0}' was removed on Uninstall", "testName2")); + + // clean up + UserVerifier.DeleteLocalUser("testName2"); + + UserVerifier.VerifyUserIsNotMemberOf(Environment.GetEnvironmentVariable(TempDomain), Environment.GetEnvironmentVariable(TempUsername), "Power Users"); + } + + // Verify that Installation fails if FailIfExisits is set. + [Fact] + public void FailsIfUserExists() + { + var productFailIfExists = this.CreatePackageInstaller("ProductFailIfExists"); + + // Create 'existinguser' + UserVerifier.CreateLocalUser("existinguser", "test123!@#"); + + try + { + productFailIfExists.InstallProduct(MSIExec.MSIExecReturnCode.ERROR_INSTALL_FAILURE); + + // Verify User still exists. + bool userExists = UserVerifier.UserExists(String.Empty, "existinguser"); + + Assert.True(userExists, String.Format("User '{0}' was removed on Rollback", "existinguser")); + } + finally + { + // clean up + UserVerifier.DeleteLocalUser("existinguser"); + } + + } + + // Verify that a user cannot be created on a domain on which you dont have create user permission. + [Fact] + public void FailsIfRestrictedDomain() + { + var productRestrictedDomain = this.CreatePackageInstaller("ProductRestrictedDomain"); + + string logFile = productRestrictedDomain.InstallProduct(MSIExec.MSIExecReturnCode.ERROR_INSTALL_FAILURE, "TEMPDOMAIN=DOESNOTEXIST"); + + // Verify expected error message in the log file + Assert.True(LogVerifier.MessageInLogFile(logFile, "ConfigureUsers: Failed to check existence of domain: DOESNOTEXIST, user: testName1 (error code 0x800706ba) - continuing")); + } + + // Verify that adding a user to a non-existent group does not fail the install when non-vital. + [Fact] + public void IgnoresMissingGroupWhenNonVital() + { + var productNonVitalGroup = this.CreatePackageInstaller("ProductNonVitalUserGroup"); + + productNonVitalGroup.InstallProduct(); + } + } +} diff --git a/src/test/msi/WixToolsetTest.MsiE2E/WixToolsetTest.MsiE2E.csproj b/src/test/msi/WixToolsetTest.MsiE2E/WixToolsetTest.MsiE2E.csproj new file mode 100644 index 00000000..ccf98042 --- /dev/null +++ b/src/test/msi/WixToolsetTest.MsiE2E/WixToolsetTest.MsiE2E.csproj @@ -0,0 +1,31 @@ + + + + + + netcoreapp3.1 + x64 + Major + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/WixToolsetTest.MsiE2E/runtests.cmd b/src/test/msi/WixToolsetTest.MsiE2E/runtests.cmd new file mode 100644 index 00000000..f609ebe4 --- /dev/null +++ b/src/test/msi/WixToolsetTest.MsiE2E/runtests.cmd @@ -0,0 +1,2 @@ +SET RuntimeTestsEnabled=true +dotnet test WixToolsetTest.MsiE2E.dll -v normal \ No newline at end of file diff --git a/src/test/msi/test_msi.cmd b/src/test/msi/test_msi.cmd new file mode 100644 index 00000000..bb9d0162 --- /dev/null +++ b/src/test/msi/test_msi.cmd @@ -0,0 +1,23 @@ +@setlocal +@pushd %~dp0 + +@set _RESULT=0 +@set _C=Debug +@set _L=%~dp0..\..\..\build\logs +:parse_args +@if /i "%1"=="release" set _C=Release +@if /i "%1"=="test" set RuntimeTestsEnabled=true +@if not "%1"=="" shift & goto parse_args + +@echo Msi integration tests %_C% + +msbuild -t:Build -Restore -p:Configuration=%_C% -warnaserror -bl:%_L%\test_msi_build.binlog || exit /b +msbuild -t:Build -Restore TestData\TestData.proj -p:Configuration=%_C% -m -bl:%_L%\test_msi_data_build.binlog || exit /b + +@if not "%RuntimeTestsEnabled%"=="true" goto :LExit + +dotnet test -c %_C% --no-build WixToolsetTest.MsiE2E -l "trx;LogFileName=%_L%\TestResults\WixToolsetTest.MsiE2E.trx" || exit /b + +:LExit +@popd +@endlocal diff --git a/src/test/test.cmd b/src/test/test.cmd index 4c80ba7d..0b29f240 100644 --- a/src/test/test.cmd +++ b/src/test/test.cmd @@ -11,6 +11,7 @@ @if not "%RuntimeTestsEnabled%"=="true" echo Build integration tests %_C% @if "%RuntimeTestsEnabled%"=="true" set _T=test&echo Run integration tests %_C% +@call msi\test_msi.cmd %_C% %_T% || exit /b @call burn\test_burn.cmd %_C% %_T% || exit /b msbuild -t:Restore dtf\DtfE2ETests.sln -p:Configuration=%_C% -nologo -m -warnaserror -bl:%_L%\dtfe2etests.binlog || exit /b -- cgit v1.2.3-55-g6feb