diff --git a/.vscode/analyzersettings.psd1 b/.vscode/analyzersettings.psd1 index 78312d2..bbf323e 100644 --- a/.vscode/analyzersettings.psd1 +++ b/.vscode/analyzersettings.psd1 @@ -1,7 +1,7 @@ @{ CustomRulePath = '.\output\RequiredModules\DscResource.AnalyzerRules' includeDefaultRules = $true - IncludeRules = @( + IncludeRules = @( # DSC Resource Kit style guideline rules. 'PSAvoidDefaultValueForMandatoryParameter', 'PSAvoidDefaultValueSwitchParameter', diff --git a/CHANGELOG.md b/CHANGELOG.md index aca1384..98ad0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,54 +5,89 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Added +### Fixed -- New helper function `Validate-SIDFormat` to verify SID value upon retrieval -in `Get-ProfilePathFromSID` +- Removed bug from `Process-RegistryProfiles` regarding populating the `FolderName` +variable. -- **Admin Detection and Environment Variable**: Added logic to detect whether the - current user is an administrator and set an environment variable - `WinProfileOps_IsAdmin` accordingly. +### Added - - If the user is an administrator, `$ENV:WinProfileOps_IsAdmin` is set to - `$true`. If not, it's set to `$false`. +#### Functions - - The environment variable is automatically removed when the module is - unloaded or when PowerShell exits. +- New helper function `Validate-SIDFormat` to verify SID value upon retrieval in +`Get-ProfilePathFromSID`. +- **Admin Detection and Environment Variable**: Added logic to detect whether +the current user is an administrator and set an environment variable +`WinProfileOps_IsAdmin` accordingly. + + - If the user is an administrator, `$env:WinProfileOps_IsAdmin` is set to + `$true`. If not, it's set to `$false`. + + - The environment variable is automatically removed when the module is unloaded + or when PowerShell exits. + - Registered an `OnRemove` script block and a `PowerShell.Exiting` event to - ensure cleanup of the environment variable on module removal or session exit. + ensure cleanup of the environment variable on module removal or session exit. + +- **Remove-UserProfilesFromRegistry**: Added a new function to remove user profiles +from the Windows registry based on SIDs, Usernames, or UserProfile objects. + + - Supports three parameter sets: `UserProfileSet`, `SIDSet`, and `UserNameSet`. + + - Can be run in `AuditOnly` mode, where no actual deletion is performed, or + in deletion mode where profiles are removed. + + - Includes a `Force` switch to bypass confirmation prompts and a + `ComputerName` parameter for targeting remote computers. + + - Graceful error handling and logging for cases where the registry key cannot + be opened or profiles cannot be processed for specific computers. + +#### Environment Variables + +- **`$env:WinProfileOps_IsAdmin`**: A boolean value that determines if the current + user has administrative privileges. This is set by checking the user's security + role against the built-in Administrator group using Windows security principals. + +- **`$env:WinProfileOps_RegistryPath`**: Specifies the registry path used to + manage user profiles. Default value: `"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"`. -- **Get-SIDProfileInfoFallback**: Introduced a new fallback function - `Get-SIDProfileInfoFallback` that retrieves non-special user profile - information using the CIM/WMI method. +- **`$env:WinProfileOps_RegistryHive`**: Defines the registry hive to use, which + is set to `LocalMachine` by default. + +- **`$env:WinProfileOps_RegBackUpDirectory`**: Specifies the directory where + registry backups are stored. Default value: `"C:\LHStuff\RegBackUp"`. + +- **`$env:WinProfileOps_ProfileFolderPath`**: The profile folder path, defaulting + to the system drive's `Users` folder. Example: `"C:\Users"`. ### Changed - **Get-UserProfilesFromRegistry**: Updated the function to handle scenarios where the current user does not have administrative privileges. - + - The function now checks if the user is an administrator by evaluating the `WinProfileOps_IsAdmin` environment variable. - + - If the user has administrator privileges, the function retrieves user profiles from the registry using `Get-SIDProfileInfo`. - + - If the user lacks administrative privileges, the function falls back to the `Get-SIDProfileInfoFallback` method, which retrieves user profiles using CIM/WMI without requiring registry access. - - - A warning is logged when the fallback method is used, indicating that - special system accounts are excluded. + + - A warning is logged when the fallback method is used, indicating that special + system accounts are excluded. - Refactored `Process-RegistryProfiles` to better account for access denied errors -when testing profile paths with `Test-FolderExists` + when testing profile paths with `Test-FolderExists`. - Updated `UserProfile` object creation in `Test-OrphanedProfile` for - `$AccessError` Scenarios + `$AccessError` scenarios. -- Module is now using `WinRegOps` Version `0.4.0` for more refined registry value -retrieval +- The module is now using `WinRegOps` version `0.4.0` for more refined registry + value retrieval. ## [0.2.0] - 2024-09-12 @@ -97,7 +132,7 @@ retrieval - These supporting functions are now utilized within `Invoke-UserProfileAudit` to audit user profiles from both the file system and registry sources. - - **`Process-RegistryProfiles`**: + - **`Process-RegistryProfiles`**: - Processes profiles retrieved from the registry, compares them with folder profiles, and identifies orphaned profiles. @@ -128,7 +163,6 @@ an internal function for `Get-RegistryUserProfiles` - Optimized function behavior to handle scenarios with no SIDs, invalid SID formats, and missing `ProfileImagePath` values gracefully. - - **`Get-UserFolders`** - The function now logs errors when folder retrieval fails, improving diagnostic feedback. @@ -173,7 +207,6 @@ an internal function for `Get-RegistryUserProfiles` - Returns an empty array `@()` when an error occurs while accessing the user folders, logging an error message. - - **`Invoke-UserProfileAudit`** - Renamed the previous `Get-AllUserProfiles` function to `Invoke-UserProfileAudit`. - Added `Get-AllUserProfiles` as an alias for `Invoke-UserProfileAudit` diff --git a/README.md b/README.md index 78f772c..7823b75 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,18 @@ computers. - **Retrieve user profile information** from both the registry and the file system (local and remote). + - **Detect orphaned profiles**, such as profiles missing from the file system or registry. + - **Filter and exclude special accounts** like system or service accounts (e.g., `defaultuser0`, `S-1-5-18`). + - **Remote profile management** with support for handling user profiles across different systems. + - **Error handling** for permission issues, unreachable systems, and missing data. + - **Class-based profile objects** for easy integration with other automation tasks or scripts. @@ -47,14 +52,17 @@ computers. ## Typical Use Cases -- **Cleaning up orphaned profiles** after system migrations, user deactivations, or - profile corruption. +- **Cleaning up orphaned profiles** after system migrations, +user deactivations, or profile corruption. + - **Managing user profiles in large-scale environments**, such as terminal servers, Citrix environments, or multi-user systems. + - **Excluding system accounts** from profile cleanup operations to prevent accidental deletion of important system profiles. -- **System maintenance routines** that include profile validation and management as - part of a broader system health check. + +- **System maintenance routines** that include profile validation + and management as part of a broader system health check. --- @@ -71,9 +79,9 @@ You have two options to install **WinProfileOps**: Install-Module -Name WinProfileOps ``` -2. **Install from GitHub Releases** +1. **Install from GitHub Releases** You can also download the latest release from the - [GitHub Releases page](https://github.com/LarryWisherMan/WinProfileOps/releases). + [GitHub Releases page](https://github.com/LarryWisherMan/WinProfileOps/releases). Download the `.zip` file, extract it, and place it in one of your `$PSModulePath` directories. @@ -116,7 +124,8 @@ This retrieves user profiles from the registry on `LocalHost`. #### Example 4: Auditing User Profiles -Use the `Invoke-UserProfileAudit` function to audit profiles across the file system and +Use the `Invoke-UserProfileAudit` function to audit profiles across the file + system and registry: ```powershell @@ -126,6 +135,36 @@ $allProfiles = Invoke-UserProfileAudit -ComputerName "Server01" This audits user profiles on `Server01`, returning both file system and registry profile information. +#### Example 5: Removing User Profiles from the Registry + +Use the `Remove-UserProfilesFromRegistry` function to remove user profiles from + the Windows registry based on SIDs, Usernames, or UserProfile objects: + +- Remove profiles by SIDs: + + ```powershell + Remove-UserProfilesFromRegistry -SIDs "S-1-5-21-1234567890-1", "S-1-5-21-1234567890-2" + ``` + +- Remove profiles by usernames on a remote computer: + + ```powershell + Remove-UserProfilesFromRegistry -Usernames "john.doe", "jane.smith" + -ComputerName "Server01" -Force -Confirm:$false + ``` + +- Audit user profiles before removal: + + ```powershell + Remove-UserProfilesFromRegistry -UserProfiles $userProfileList -AuditOnly + ``` + +**Note:** To bypass any confirmation prompts during profile removal, both the + `-Force` switch and `-Confirm:$false` must be specified. + +This allows you to either remove or audit profiles based on their SIDs, +usernames, or UserProfile objects. + --- ## Key Functions @@ -138,41 +177,33 @@ profile information. registry. - **`Get-UserProfilesFromFolders`**: Retrieves user profile folders from the file system. +- **`Remove-UserProfilesFromRegistry`**: Removes user profiles from the Windows +registry based on SIDs, Usernames, or UserProfile objects, + with options for audit-only mode or forced removal. --- -## Upcoming Features - -### `Remove-UserProfile` (Coming Soon!) +## Environment Variables -The `Remove-UserProfile` function will provide the ability to remove user -profiles safely from both the registry and the file system. Here are some of the - key features being tested for this functionality: +The **WinProfileOps** module uses several environment variables to configure +certain default paths and behaviors. These variables are automatically set +when the module is loaded and can be adjusted as needed: -- **Safely remove user profiles** either from the file system (i.e., user profile - folders) or from the Windows registry. +- **`$env:WinProfileOps_IsAdmin`**: Determines if the current user has +administrative privileges. It is determined by the current context of the +user. + +- **`$env:WinProfileOps_RegistryPath`**: Specifies the registry path used for + managing user profiles. Default value: `"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"`. -- **Flexible inputs**: Accepts a UserProfile object, a username, or a SID for - profile removal. +- **`$env:WinProfileOps_RegistryHive`**: Defines the registry hive used in + operations, set to `LocalMachine` by default. -- **Powerful safeguards**: Uses `ShouldProcess`, `-WhatIf`, and `-Confirm` to - ensure that deletion is intentional and carefully reviewed. - -- **Handles special accounts**: Prevents accidental removal of critical system - or service accounts (e.g., `S-1-5-18`). - -- **Remote profile removal**: Enables profile deletion from both local and remote - computers. - -- **Verbose and error handling**: Logs every action taken and handles errors to - provide full transparency and avoid unexpected issues. - -The feature is being heavily tested to ensure safety, reliability, and accuracy. - ---- +- **`$env:WinProfileOps_RegBackUpDirectory`**: Specifies the directory where + registry backups are stored. Default value: `"C:\LHStuff\RegBackUp"`. -## Contributing +- **`$env:WinProfileOps_ProfileFolderPath`**: The profile folder path, defaulting + to `"C:\Users"`, but can be customized based on the system's configuration. -Contributions are welcome! Feel free to fork the repository, submit pull requests, -or report issues. You can contribute by adding new features, improving the existing -code, or enhancing the documentation. +These variables are set automatically when the module is imported and are cleared + when the module is unloaded or the PowerShell session ends. diff --git a/RequiredModules.psd1 b/RequiredModules.psd1 index b647772..c3793c0 100644 --- a/RequiredModules.psd1 +++ b/RequiredModules.psd1 @@ -23,7 +23,7 @@ #'WinRegOps' = '0.3.0' 'WinRegOps' = @{ - Version = '0.4.0-preview0001' + Version = '0.4.0-preview0003' Parameters = @{ AllowPrerelease = $true Repository = "PSGallery" diff --git a/source/Classes/ProfileDeletionResult.ps1 b/source/Classes/ProfileDeletionResult.ps1 index 4f0a198..824e55e 100644 --- a/source/Classes/ProfileDeletionResult.ps1 +++ b/source/Classes/ProfileDeletionResult.ps1 @@ -1,16 +1,51 @@ -class ProfileDeletionResult { +class ProfileDeletionResult +{ [string]$SID [string]$ProfilePath [bool]$DeletionSuccess [string]$DeletionMessage [string]$ComputerName - # Constructor to initialize the properties - ProfileDeletionResult([string]$sid, [string]$profilePath, [bool]$deletionSuccess, [string]$deletionMessage, [string]$computerName) { + # Constructor 1: Full constructor + ProfileDeletionResult([string]$sid, [string]$profilePath, [bool]$deletionSuccess, [string]$deletionMessage, [string]$computerName) + { $this.SID = $sid $this.ProfilePath = $profilePath $this.DeletionSuccess = $deletionSuccess $this.DeletionMessage = $deletionMessage $this.ComputerName = $computerName } + + # Constructor 2: Only SID and DeletionSuccess, with default values for others + ProfileDeletionResult([string]$sid, [bool]$deletionSuccess) + { + $this.SID = $sid + $this.ProfilePath = $null + $this.DeletionSuccess = $deletionSuccess + if ($deletionSuccess) + { + $this.DeletionMessage = "Operation successful" + } + else + { + $this.DeletionMessage = "Operation failed" + } + $this.ComputerName = $env:COMPUTERNAME + } + + # Constructor 3: Minimal constructor with defaults for all except SID + ProfileDeletionResult([string]$sid) + { + $this.SID = $sid + $this.ProfilePath = $null + $this.DeletionSuccess = $false + $this.DeletionMessage = "No action performed" + $this.ComputerName = $env:COMPUTERNAME + } + + # Optional method + [string] ToString() + { + return "[$($this.SID)] DeletionSuccess: $($this.DeletionSuccess), Message: $($this.DeletionMessage)" + } } diff --git a/source/Classes/UserProfile.ps1 b/source/Classes/UserProfile.ps1 index 5d981ca..f32f23c 100644 --- a/source/Classes/UserProfile.ps1 +++ b/source/Classes/UserProfile.ps1 @@ -6,6 +6,10 @@ class UserProfile [string]$OrphanReason = $null [string]$ComputerName [bool]$IsSpecial + [string] GetUserNameFromPath() { + return [System.IO.Path]::GetFileName($this.ProfilePath) # Extract the leaf (username) from the ProfilePath + } + # Constructor to initialize the properties UserProfile([string]$sid, [string]$profilePath, [bool]$isOrphaned, [string]$orphanReason, [string]$computerName, [bool]$isSpecial) diff --git a/source/Private/Get-SIDProfileInfoFallback.ps1 b/source/NotImplemented/Get-SIDProfileInfoFallback.ps1 similarity index 100% rename from source/Private/Get-SIDProfileInfoFallback.ps1 rename to source/NotImplemented/Get-SIDProfileInfoFallback.ps1 diff --git a/tests/Unit/Private/Get-SIDProfileInfoFallback.tests.ps1 b/source/NotImplemented/Get-SIDProfileInfoFallback.tests.ps1 similarity index 100% rename from tests/Unit/Private/Get-SIDProfileInfoFallback.tests.ps1 rename to source/NotImplemented/Get-SIDProfileInfoFallback.tests.ps1 diff --git a/source/NotImplemented/Get-UserProfileLastUseTime.ps1 b/source/NotImplemented/Get-UserProfileLastUseTime.ps1 new file mode 100644 index 0000000..2d2dcae --- /dev/null +++ b/source/NotImplemented/Get-UserProfileLastUseTime.ps1 @@ -0,0 +1,41 @@ +function Get-UserProfileLastUseTime +{ + [CmdletBinding()] + param ( + [string]$ComputerName = $env:COMPUTERNAME, + [string]$SystemDrive = $env:SystemDrive, + [switch]$UseCitrixLog + ) + + if ($UseCitrixLog) + { + $basePath = "$SystemDrive\Users\*\AppData\Local\Citrix\Receiver\Toaster_.log" + } + else + { + $BasePath = "$SystemDrive\Users\*\AppData\Local\Microsoft\Windows\UsrClass.dat" + + } + + # Check if we are querying a local or remote computer + $isLocal = ($ComputerName -eq $env:COMPUTERNAME) + + # Define user name expression based on whether it's local or remote + if ($isLocal) + { + $UserNameExpression = @{Label = "User"; Expression = { ($_.directory).tostring().split("\")[2] } } + } + else + { + $UserNameExpression = @{Label = "User"; Expression = { ($_.directory).tostring().split("\")[5] } } + } + + # Get the correct directory path (local or remote) + $Path = Get-DirectoryPath -BasePath $BasePath -ComputerName $ComputerName -IsLocal:$isLocal + + # Define a ComputerName column for output + $ComputerNameExpression = @{Label = "ComputerName"; Expression = { $ComputerName } } + + # Retrieve the UsrClass.dat file's last write time for each user profile + Get-ChildItem -Path $Path -Force | Select-Object $UserNameExpression, LastWriteTime, $ComputerNameExpression +} diff --git a/source/NotImplemented/Remove-OrphanedProfiles.ps1 b/source/NotImplemented/Remove-OrphanedProfiles.ps1 index 770a93f..048702c 100644 --- a/source/NotImplemented/Remove-OrphanedProfiles.ps1 +++ b/source/NotImplemented/Remove-OrphanedProfiles.ps1 @@ -28,7 +28,7 @@ function Remove-OrphanedProfiles [string]$ComputerName, [Parameter(Mandatory = $false)] - [string]$ProfileFolderPath = "$env:SystemDrive\Users", + [string]$ProfileFolderPath = $env:WinProfileOps_ProfileFolderPath, [switch]$IgnoreSpecial ) @@ -57,6 +57,3 @@ function Remove-OrphanedProfiles # Step 4: Return the results of the removal process return $removalResults } - - - diff --git a/source/NotImplemented/Remove-ProfilesForSIDs.ps1 b/source/NotImplemented/Remove-ProfilesForSIDs.ps1 index a812fde..2f2dd69 100644 --- a/source/NotImplemented/Remove-ProfilesForSIDs.ps1 +++ b/source/NotImplemented/Remove-ProfilesForSIDs.ps1 @@ -90,3 +90,70 @@ function Remove-ProfilesForSIDs # Return the array of deletion results return $deletionResults } + + +function Remove-ProfilesForSIDs { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory = $true)] + [string[]]$SIDs, # Accept multiple SIDs as an array + + [Parameter(Mandatory = $false)] + [string]$ComputerName = $env:COMPUTERNAME # Default to local computer + ) + + # Base registry path for profiles + $RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" + $deletionResults = @() + + # Loop through each SID and process deletion + foreach ($sid in $SIDs) { + try { + # Full path to the profile SID key + $fullRegistryPath = Join-Path -Path $RegistryPath -ChildPath $sid + + # Check if the profile key exists before trying to delete + $profileKeyExists = Test-Path "HKLM:\$fullRegistryPath" + + if (-not $profileKeyExists) { + # If the profile does not exist, add a result indicating it wasn't found + $deletionResults += [PSCustomObject]@{ + SID = $sid + ProfilePath = $null + Success = $false + Message = "Profile SID '$sid' not found in registry." + Computer = $ComputerName + } + continue + } + + # Attempt to remove the profile using Remove-RegistrySubKey + Remove-RegistrySubKey -RegistryHive 'LocalMachine' -RegistryPath $RegistryPath -SubKeyName $SID -ComputerName $ComputerName -ThrowOnMissingSubKey $false + + # Add a result indicating success + $deletionResults += [PSCustomObject]@{ + SID = $sid + ProfilePath = $fullRegistryPath + Success = $true + Message = "Profile SID '$sid' removed successfully." + Computer = $ComputerName + } + } + catch { + # Handle any errors that occur during deletion + Write-Error "An error occurred while processing SID '$sid'. $_" + + # Add a result indicating failure due to an error + $deletionResults += [PSCustomObject]@{ + SID = $sid + ProfilePath = $null + Success = $false + Message = "Error occurred while processing SID '$sid'. Error: $_" + Computer = $ComputerName + } + } + } + + # Return the array of deletion results + return $deletionResults +} diff --git a/source/Private/Get-SIDProfileInfo.ps1 b/source/Private/Get-SIDProfileInfo.ps1 index bd3a9f6..926d751 100644 --- a/source/Private/Get-SIDProfileInfo.ps1 +++ b/source/Private/Get-SIDProfileInfo.ps1 @@ -11,6 +11,9 @@ .PARAMETER ComputerName The name of the computer from which to retrieve profile information. Defaults to the local computer. +.parameter RegistryPath + The registry path to the ProfileList key. Defaults to "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList". + .EXAMPLE Get-SIDProfileInfo -ComputerName "Server01" Retrieves profile information for all valid SIDs stored in the registry on "Server01". @@ -39,11 +42,11 @@ function Get-SIDProfileInfo [OutputType([PSCustomObject[]])] [CmdletBinding()] param ( - [string]$ComputerName = $env:COMPUTERNAME + [string]$ComputerName = $env:COMPUTERNAME, + [string]$RegistryPath = $env:WinProfileOps_RegistryPath ) - $RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" - $ProfileListKey = Open-RegistryKey -RegistryPath $RegistryPath -ComputerName $ComputerName -Writable $false + $ProfileListKey = Open-RegistryKey -RegistryPath $RegistryPath -ComputerName $ComputerName -Writable $false -RegistryHive $env:WinProfileOps_RegistryHive # Handle null or empty registry key if (-not $ProfileListKey) diff --git a/source/Private/Get-UserFolders.ps1 b/source/Private/Get-UserFolders.ps1 index 891dd9b..a8ffc24 100644 --- a/source/Private/Get-UserFolders.ps1 +++ b/source/Private/Get-UserFolders.ps1 @@ -47,7 +47,7 @@ function Get-UserFolders [CmdletBinding()] param ( [string]$ComputerName = $env:COMPUTERNAME, - [string]$ProfileFolderPath = "$env:SystemDrive\Users" + [string]$ProfileFolderPath = $env:WinProfileOps_ProfileFolderPath ) $IsLocal = ($ComputerName -eq $env:COMPUTERNAME) diff --git a/source/Private/Helpers/Get-SIDFromUsername.ps1 b/source/Private/Helpers/Get-SIDFromUsername.ps1 new file mode 100644 index 0000000..3309955 --- /dev/null +++ b/source/Private/Helpers/Get-SIDFromUsername.ps1 @@ -0,0 +1,67 @@ +<# +.SYNOPSIS +Retrieves the Security Identifier (SID) for a given username from a specified computer, defaulting to the local computer if no computer name is provided. + +.DESCRIPTION +The `Get-SIDFromUsername` function queries the specified computer using WMI (CIM) to retrieve the SID associated with a given username. If the `ComputerName` parameter is not provided, the function defaults to the local computer. The function uses the `Get-CimInstance` cmdlet to perform the lookup on the remote or local computer. If the user exists and the SID is found, it is returned. If no SID is found or an error occurs, a warning message is displayed, and the function returns `$null`. + +.PARAMETER Username +Specifies the username for which to retrieve the SID. This parameter is mandatory. + +.PARAMETER ComputerName +Specifies the name of the computer where the user account exists. This parameter is optional and defaults to the local computer (`localhost`). You can specify either a local or remote computer. + +.EXAMPLE +Get-SIDFromUsername -Username 'JohnDoe' -ComputerName 'Server01' + +Description: +This command retrieves the SID for the user 'JohnDoe' from the computer 'Server01'. If the user exists on the computer and has a SID, it will be returned; otherwise, a warning will be displayed. + +.EXAMPLE +Get-SIDFromUsername -Username 'LocalAdmin' + +Description: +This command retrieves the SID for the user 'LocalAdmin' from the local computer (localhost) since no `ComputerName` is provided. If the user exists on the local computer and has a SID, it will be returned; otherwise, a warning will be displayed. + +.EXAMPLE +Get-SIDFromUsername -Username 'DomainUser' -ComputerName 'DomainController' + +Description: +This command retrieves the SID for the user 'DomainUser' from the remote computer 'DomainController'. If the user exists on the specified computer and has a SID, it will be returned; otherwise, a warning will be displayed. + +.NOTES +If the `ComputerName` is not provided, it defaults to the local computer. +#> + +function Get-SIDFromUsername +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Username, + + [Parameter(Mandatory = $false)] + [string]$ComputerName = $env:COMPUTERNAME + ) + + try + { + # Query WMI to get the SID for the given username + $userAccount = Get-CimInstance -Class Win32_UserAccount -ComputerName $ComputerName -Filter "Name = '$Username'" + + if ($userAccount -and $userAccount.SID) + { + return $userAccount.SID + } + else + { + Write-Warning "Could not find SID for username $Username on $ComputerName." + return $null + } + } + catch + { + Write-Warning "An error occurred while trying to resolve SID for username $Username on $ComputerName. Error: $_" + return $null + } +} diff --git a/source/Private/Helpers/New-DirectoryIfNeeded.ps1 b/source/Private/Helpers/New-DirectoryIfNeeded.ps1 new file mode 100644 index 0000000..9fe1ed1 --- /dev/null +++ b/source/Private/Helpers/New-DirectoryIfNeeded.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS +Creates a directory if it does not already exist. + +.DESCRIPTION +The `New-DirectoryIfNeeded` function checks if the specified directory exists. If it doesn't, the function will create the directory and return the created directory object. If the directory already exists, the function returns `$true`. In case of any errors during directory creation, the function returns `$false` and logs the error. + +.PARAMETER Directory +Specifies the full path of the directory to check or create. This parameter is mandatory. If the directory path is `null`, empty, or contains only whitespace, the function throws an error. + +.EXAMPLE +New-DirectoryIfNeeded -Directory 'C:\Temp\NewFolder' + +Description: +This command checks if the directory 'C:\Temp\NewFolder' exists. If it doesn't, the directory will be created. If the directory already exists, the function will return `$true`. + +.EXAMPLE +New-DirectoryIfNeeded -Directory 'D:\Logs' + +Description: +This command checks if the directory 'D:\Logs' exists. If it does not, the function will create the directory. If the directory already exists, it returns `$true`. + +.EXAMPLE +$directory = New-DirectoryIfNeeded -Directory 'C:\Data\Reports' + +Description: +This command attempts to create the directory 'C:\Data\Reports' if it doesn't exist and assigns the result to `$directory`. If successful, `$directory` will contain the created directory object. If the directory already exists, `$true` will be assigned to `$directory`. + +.NOTES +If the directory path is invalid or if an error occurs during the creation process, the function writes an error message and returns `$false`. +#> + +function New-DirectoryIfNeeded +{ + param ( + [Parameter(Mandatory = $true)] + [string]$Directory + ) + + try + { + # Check if the Directory parameter is null or an empty string + if ([string]::IsNullOrWhiteSpace($Directory)) + { + throw [System.ArgumentException]::new("The 'Directory' parameter cannot be null or empty.") + } + + # If the directory does not exist, attempt to create it + if (-not (Test-Path -Path $Directory)) + { + $newDirectory = New-Item -Path $Directory -ItemType Directory -Force -ErrorAction Stop + return $newDirectory + } + + # If the directory exists, return $true + return $true + } + catch + { + Write-Error "Failed to create directory: $Directory. Error: $_" + return $false + } +} diff --git a/source/Private/Helpers/ShouldContinueWrapper.ps1 b/source/Private/Helpers/ShouldContinueWrapper.ps1 new file mode 100644 index 0000000..e08a021 --- /dev/null +++ b/source/Private/Helpers/ShouldContinueWrapper.ps1 @@ -0,0 +1,44 @@ +<# +.SYNOPSIS +Handles user confirmation prompts using the `ShouldContinue` method. + +.DESCRIPTION +The `ShouldContinueWrapper` function prompts the user to confirm whether they want to proceed with an operation. It uses the `ShouldContinue` method from the execution context to display a message to the user. The function logs whether the user chose to continue or not and returns the result. + +.PARAMETER Context +Specifies the execution context, typically used to invoke the `ShouldContinue` method. + +.PARAMETER QueryMessage +Specifies the message to display to the user asking if they are sure they want to proceed. + +.PARAMETER CaptionMessage +Specifies the caption of the confirmation prompt, providing additional context about the operation. + +.EXAMPLE +$context = Get-ExecutionContext +ShouldContinueWrapper -Context $context -QueryMessage "Are you sure you want to delete these items?" -CaptionMessage "Confirm Deletion" + +Description: +Prompts the user with the message "Are you sure you want to delete these items?" and the caption "Confirm Deletion". The function returns `$true` if the user chooses to continue, otherwise it returns `$false`. + +.NOTES +This function assumes that it is called within an appropriate execution context where `ShouldContinue` can be invoked. +#> +function ShouldContinueWrapper +{ + param ( + [Parameter(Mandatory = $true)] + $Context, + + [Parameter(Mandatory = $true)] + [string]$QueryMessage, + + [Parameter(Mandatory = $true)] + [string]$CaptionMessage + ) + $result = $Context.ShouldContinue($QueryMessage, $CaptionMessage) + + Write-Verbose "User chose to continue: $result" + + return $result +} diff --git a/source/Private/Helpers/ShouldProcessWrapper.ps1 b/source/Private/Helpers/ShouldProcessWrapper.ps1 new file mode 100644 index 0000000..d8fee8d --- /dev/null +++ b/source/Private/Helpers/ShouldProcessWrapper.ps1 @@ -0,0 +1,56 @@ +<# +.SYNOPSIS +Handles the user confirmation for actions using the `ShouldProcess` method. + +.DESCRIPTION +The `ShouldProcessWrapper` function prompts the user to confirm whether they want to proceed with a specified action on a specified target. It uses the `ShouldProcess` method from the execution context, logging the action and the target for verbose output. The function returns the result of the user's decision, allowing the calling function to proceed or halt based on the confirmation. + +.PARAMETER Context +Specifies the execution context, typically used to invoke the `ShouldProcess` method. + +.PARAMETER Target +Specifies the target of the action, such as a computer, file, or registry path, that the user is being asked to confirm. + +.PARAMETER ActionMessage +Specifies the action that will be performed on the target, such as "Deleting", "Modifying", or "Stopping a service." + +.EXAMPLE +$context = Get-ExecutionContext +ShouldProcessWrapper -Context $context -Target "Server01" -ActionMessage "Delete profiles" + +Description: +Prompts the user to confirm if they want to proceed with deleting profiles from "Server01". The function logs the action and the target, then returns `$true` if the user agrees, otherwise returns `$false`. + +.EXAMPLE +ShouldProcessWrapper -Context $context -Target "C:\Temp\File.txt" -ActionMessage "Remove the file" + +Description: +Prompts the user with the message "Remove the file" for the target file "C:\Temp\File.txt". It logs the action and returns the user's response. + +.NOTES +This function is typically used in cmdlets or scripts that support the `ShouldProcess` functionality to allow confirmation before destructive or critical actions. +#> + +function ShouldProcessWrapper +{ + param ( + [Parameter(Mandatory = $true)] + $Context, + + [Parameter(Mandatory = $true)] + [string]$Target, + + [Parameter(Mandatory = $true)] + [string]$ActionMessage + ) + + # Log the action message for verbose output + Write-Verbose "About to perform action: $ActionMessage on $Target" + + # Use the ShouldProcess method from the context + $result = $Context.ShouldProcess($Target, $ActionMessage) + + Write-Verbose "User chose to process: $result" + + return $result +} diff --git a/source/Private/Helpers/Test-EnvironmentVariable.ps1 b/source/Private/Helpers/Test-EnvironmentVariable.ps1 new file mode 100644 index 0000000..f9fa276 --- /dev/null +++ b/source/Private/Helpers/Test-EnvironmentVariable.ps1 @@ -0,0 +1,36 @@ +<# +.SYNOPSIS +Validates the presence of a specific environment variable. + +.DESCRIPTION +The Test-EnvironmentVariable function checks if the specified environment variable exists. +If the variable is found, it returns its value. If not, an error is thrown. + +.PARAMETER Name +The name of the environment variable to check. + +.EXAMPLE +Test-EnvironmentVariable -Name 'Path' + +This command checks if the 'Path' environment variable is present and returns its value if found. + +.OUTPUTS +String (Value of the environment variable) + +.NOTES +This function will throw an error if the environment variable is missing from sesion. +#> +function Test-EnvironmentVariable +{ + param ([string]$Name) + + # Dynamically retrieve the environment variable + $value = Get-Item -Path "Env:$Name" -ErrorAction SilentlyContinue + + if (-not $value) + { + throw "Missing required environment variable: $Name" + } + + return $value.Value +} diff --git a/source/Private/Helpers/Update-JsonFile.ps1 b/source/Private/Helpers/Update-JsonFile.ps1 new file mode 100644 index 0000000..67fba2a --- /dev/null +++ b/source/Private/Helpers/Update-JsonFile.ps1 @@ -0,0 +1,76 @@ +<# +.SYNOPSIS +Updates an existing JSON file with new registry data or creates a new file if one doesn't exist. + +.DESCRIPTION +The `Update-JsonFile` function checks if a specified JSON file exists and either updates it with new registry data or creates a new file. If the file exists, it reads the current data, appends the new registry data, and writes it back to the file. If the file does not exist, it creates a new file with the provided data. The function handles registry data in a generic array format. + +.PARAMETER OutputFile +Specifies the path to the JSON file that should be updated or created. This parameter is mandatory. + +.PARAMETER RegistryData +Specifies the new registry data to add to the JSON file. This should be passed as an array. The function will append this data to any existing data in the file, or it will create a new file with this data if the file doesn't exist. + +.EXAMPLE +$registryData = @( + @{ Name = 'HKEY_LOCAL_MACHINE\Software\TestKey'; Value = 'TestValue1' }, + @{ Name = 'HKEY_LOCAL_MACHINE\Software\AnotherKey'; Value = 'TestValue2' } +) +Update-JsonFile -OutputFile 'C:\Temp\RegistryData.json' -RegistryData $registryData + +Description: +This example updates the file `RegistryData.json` in `C:\Temp` with the provided `$registryData`. If the file doesn't exist, it will be created. + +.EXAMPLE +Update-JsonFile -OutputFile 'C:\Config\Settings.json' -RegistryData @(@{ Name = 'HKEY_CURRENT_USER\Software\MyApp'; Value = 'UserSetting' }) + +Description: +This command appends the new registry data to the `Settings.json` file located in `C:\Config`. If the file doesn't exist, a new file is created with the registry data. + +.OUTPUTS +None. This function writes updated data back to the file specified in the `OutputFile` parameter. + +.NOTES +- The function automatically handles appending new data to existing arrays in the JSON file. +- JSON files are written with a depth of 10 to ensure nested objects are properly serialized. + +#> +function Update-JsonFile +{ + param ( + [Parameter(Mandatory = $true)] + [string]$OutputFile, + + [Parameter(Mandatory = $true)] + [array]$RegistryData # Generic data for registry keys + ) + + if (Test-Path $OutputFile) + { + # Get the existing data and convert it from JSON + $existingData = Get-Content -Path $OutputFile -Raw | ConvertFrom-Json + + # Ensure existing data is an array, wrap in an array if necessary + if (-not ($existingData -is [System.Collections.IEnumerable])) + { + $existingData = @($existingData) + } + + # Ensure the existing data is an array of objects + if ($existingData -isnot [array]) + { + $existingData = @($existingData) + } + + # Concatenate the existing data and the new data + $combinedData = @($existingData + $RegistryData) + + # Write the updated data back to the file + $combinedData | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputFile -Confirm:$false + } + else + { + # Create a new JSON file with the provided registry data + $RegistryData | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputFile + } +} diff --git a/source/Private/Remove-RegistryKeyForSID.ps1 b/source/Private/Remove-RegistryKeyForSID.ps1 deleted file mode 100644 index 302632b..0000000 --- a/source/Private/Remove-RegistryKeyForSID.ps1 +++ /dev/null @@ -1,54 +0,0 @@ -<# -.SYNOPSIS - Deletes a registry key associated with a specific SID from the ProfileList. -.DESCRIPTION - The Remove-RegistryKeyForSID function deletes the registry key corresponding to a specific Security Identifier (SID) from the ProfileList in the Windows registry. It supports confirmation prompts and simulates actions with the -WhatIf parameter. -.PARAMETER SID - The Security Identifier (SID) for which the registry key should be deleted. -.PARAMETER ProfileListKey - The opened registry key representing the ProfileList where the profile's SID is located. -.PARAMETER ComputerName - The name of the computer where the profile registry key resides. By default, this is the current computer. -.EXAMPLE - Remove-RegistryKeyForSID -SID "S-1-5-21-123456789-1001" -ProfileListKey $profileListKey -ComputerName "Server01" - Deletes the registry key for the specified SID from the ProfileList on "Server01". -.NOTES - This function supports 'ShouldProcess', so it can be used in conjunction with the -WhatIf or -Confirm parameters to simulate the deletion. - It also includes error handling to ensure any failure during the registry key deletion is captured. -#> - -function Remove-RegistryKeyForSID -{ - # Deletes a single registry key for a SID. - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] - param ( - [Parameter(Mandatory = $true)] - [string]$SID, - - [Parameter(Mandatory = $true)] - [Microsoft.Win32.RegistryKey]$ProfileListKey, - - [Parameter(Mandatory = $true)] - [string]$ComputerName = $env:COMPUTERNAME - ) - - try - { - # Check if ShouldProcess is approved (with -WhatIf and -Confirm support) - if ($PSCmdlet.ShouldProcess("SID: $SID on $ComputerName", "Remove registry key")) - { - # Use the general Remove-RegistrySubKey function to delete the SID's subkey - return Remove-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $SID -ComputerName $ComputerName -Confirm:$false - } - else - { - Write-Verbose "Removal of registry key for SID '$SID' was skipped." - return $false - } - } - catch - { - Write-Error "Failed to remove the profile registry key for SID '$SID' on $ComputerName. Error: $_" - return $false - } -} diff --git a/source/Private/Remove-SIDProfile.ps1 b/source/Private/Remove-SIDProfile.ps1 deleted file mode 100644 index a8caf96..0000000 --- a/source/Private/Remove-SIDProfile.ps1 +++ /dev/null @@ -1,102 +0,0 @@ -<# -.SYNOPSIS - Coordinates the deletion of a profile registry key for a given SID. - -.DESCRIPTION - The Remove-SIDProfile function removes the registry key associated with a specific Security Identifier (SID) from the ProfileList on the specified computer. It supports confirmation prompts and -WhatIf scenarios by using the ShouldProcess pattern. The function also handles errors that occur during the deletion process and returns a ProfileDeletionResult object indicating success or failure. - -.PARAMETER SID - The Security Identifier (SID) of the profile to be deleted. - -.PARAMETER ProfileListKey - The registry key representing the ProfileList from which the SID's registry key will be removed. - -.PARAMETER ComputerName - The name of the computer where the profile registry key resides. Defaults to the current computer. - -.PARAMETER ProfilePath - The file path of the profile to be deleted, used for logging purposes in the ProfileDeletionResult object. - -.OUTPUTS - [ProfileDeletionResult] - An object that indicates whether the profile registry key was successfully deleted or if the action was skipped or failed. Includes the SID, ProfilePath, DeletionSuccess status, DeletionMessage, and ComputerName. - -.EXAMPLE - Remove-SIDProfile -SID "S-1-5-21-123456789-1001" -ProfileListKey $profileListKey -ComputerName "Server01" -ProfilePath "C:\Users\John" - Removes the registry key for the specified SID from the ProfileList on "Server01" and deletes the profile. - -.EXAMPLE - Remove-SIDProfile -SID "S-1-5-21-123456789-1001" -ProfileListKey $profileListKey -ProfilePath "C:\Users\John" -WhatIf - Simulates the removal of the profile registry key for the specified SID using the -WhatIf parameter, showing what would have been done without performing the action. - -.NOTES - - The function supports 'ShouldProcess', allowing the use of -WhatIf and -Confirm parameters for safety. - - In case of an error, the function returns a ProfileDeletionResult object with DeletionSuccess set to $false and logs the error message. - - If the action is skipped (e.g., due to -WhatIf or confirmation denial), the function returns a ProfileDeletionResult with a status indicating that the action was skipped. -#> - -function Remove-SIDProfile -{ - [outputtype([ProfileDeletionResult])] - # Coordinates the registry key deletion and provides a result for a single SID. - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] - param ( - [string]$SID, - [Microsoft.Win32.RegistryKey]$ProfileListKey, - [string]$ComputerName, - [string]$ProfilePath - ) - - try - { - # Use ShouldProcess to check if the action should proceed (with -WhatIf and -Confirm support) - if ($PSCmdlet.ShouldProcess("SID: $SID on $ComputerName", "Remove profile registry key")) - { - # Attempt to remove the registry key - $deletionSuccess = Remove-RegistryKeyForSID -SID $SID -ProfileListKey $ProfileListKey -ComputerName $ComputerName - - if ($deletionSuccess) - { - return [ProfileDeletionResult]::new( - $SID, - $ProfilePath, - $true, - "Profile registry key for SID '$SID' successfully deleted.", - $ComputerName - ) - } - else - { - return [ProfileDeletionResult]::new( - $SID, - $ProfilePath, - $false, - "Failed to delete the profile registry key for SID '$SID'.", - $ComputerName - ) - } - } - else - { - Write-Verbose "Removal of profile registry key for SID '$SID' on '$ComputerName' was skipped." - return [ProfileDeletionResult]::new( - $SID, - $ProfilePath, - $false, - "Action skipped.", - $ComputerName - ) - } - } - catch - { - Write-Error "Failed to remove the profile registry key for SID '$SID' on $ComputerName. Error: $_" - return [ProfileDeletionResult]::new( - $SID, - $ProfilePath, - $false, - "Failed to delete the profile registry key for SID '$SID'. Error: $_", - $ComputerName - ) - } -} diff --git a/source/Private/RemoveProfileReg/Backup-RegistryKeyForSID.ps1 b/source/Private/RemoveProfileReg/Backup-RegistryKeyForSID.ps1 new file mode 100644 index 0000000..866b289 --- /dev/null +++ b/source/Private/RemoveProfileReg/Backup-RegistryKeyForSID.ps1 @@ -0,0 +1,76 @@ +<# +.SYNOPSIS +Backs up a registry key associated with a specific SID to a specified directory. + +.DESCRIPTION +The `Backup-RegistryKeyForSID` function creates a backup of the registry key associated with the provided SID from a remote or local machine. It ensures that the backup directory exists before proceeding, creates a JSON representation of the registry data, and appends the backup to an existing JSON file. + +.PARAMETER SID +Specifies the Security Identifier (SID) for which the registry key backup is created. + +.PARAMETER BaseKey +Specifies the base registry key under which the SID subkey exists. + +.PARAMETER RegBackUpDirectory +Specifies the directory where the registry backup will be saved. + +.PARAMETER ComputerName +Specifies the name of the computer from which the registry key is being backed up. + +.EXAMPLE +Backup-RegistryKeyForSID -SID 'S-1-5-21-...' -BaseKey $RegistryKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + +Description: +Backs up the registry key for the specified SID from Server01 to the 'C:\Backups' directory. + +.OUTPUTS +Boolean indicating success or failure. + +.NOTES +This function relies on helper functions like `New-DirectoryIfNeeded` and `New-RegistryKeyValuesObject` to handle registry operations. +#> + +function Backup-RegistryKeyForSID +{ + param ( + [Parameter(Mandatory = $true)] + [string]$SID, + + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey]$BaseKey, + + [Parameter(Mandatory = $true)] + [string]$RegBackUpDirectory, + + [Parameter(Mandatory = $true)] + [string]$ComputerName + ) + + try + { + # Ensure the backup directory exists + $directoryCreated = New-DirectoryIfNeeded -Directory $RegBackUpDirectory + + # Check if directory creation failed + if (-not $directoryCreated) + { + Write-Error "Error creating or accessing backup directory: $RegBackUpDirectory" + return $false + } + + # Backup the registry key associated with the SID + $RegBackUpObject = New-RegistryKeyValuesObject -RegistryKey $BaseKey -ComputerName $ComputerName -SubKeyName $SID + $RegBackUpObjectJson = $RegBackUpObject.psobject.copy() + $RegBackUpObjectJson.BackUpDate = $RegBackUpObject.BackUpDate.tostring("o") + + # Update the backup JSON file with the registry data + Update-JsonFile -OutputFile "$RegBackUpDirectory\RegBackUp.json" -RegistryData $RegBackUpObjectJson + + return $true + } + catch + { + Write-Error "Error backing up registry for SID $SID`: $_" + return $false + } +} diff --git a/source/Private/RemoveProfileReg/Confirm-ProfileRemoval.ps1 b/source/Private/RemoveProfileReg/Confirm-ProfileRemoval.ps1 new file mode 100644 index 0000000..044a6c7 --- /dev/null +++ b/source/Private/RemoveProfileReg/Confirm-ProfileRemoval.ps1 @@ -0,0 +1,40 @@ +<# +.SYNOPSIS +Verifies whether a registry key for a specific SID has been successfully removed. + +.DESCRIPTION +The `Confirm-ProfileRemoval` function checks whether the registry key associated with the specified SID still exists. If the key no longer exists, the function returns `$true`; otherwise, it returns `$false`. + +.PARAMETER SID +Specifies the Security Identifier (SID) whose registry key removal is being confirmed. + +.PARAMETER BaseKey +Specifies the base registry key under which the SID subkey exists. + +.EXAMPLE +Confirm-ProfileRemoval -SID 'S-1-5-21-...' -BaseKey $RegistryKey + +Description: +Checks if the registry key for the specified SID has been successfully removed. + +.OUTPUTS +Boolean indicating whether the registry key was removed. +#> + +function Confirm-ProfileRemoval +{ + param ( + [string]$SID, + [Microsoft.Win32.RegistryKey]$BaseKey + ) + + try + { + return ($BaseKey.GetSubKeyNames() -notcontains $SID) + } + catch + { + Write-Error "Error verifying profile removal for SID $SID`: $_" + return $false + } +} diff --git a/source/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.ps1 b/source/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.ps1 new file mode 100644 index 0000000..3b303ab --- /dev/null +++ b/source/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.ps1 @@ -0,0 +1,157 @@ +<# +.SYNOPSIS +Removes user profile registry entries from local or remote computers, with optional confirmation. + +.DESCRIPTION +The `Invoke-UserProfileRegRemoval` function processes user profiles for removal based on Security Identifiers (SIDs). It retrieves profiles from a specified registry path and profile folder, performs an audit, and optionally prompts for confirmation before removal. The `Force` switch can bypass the confirmation prompt, and the `AuditOnly` switch allows auditing without any removal action. + +If the registry key cannot be opened or the audit fails, the function terminates early to prevent further processing. + +.PARAMETER ComputerName +Specifies the name of the computer where the profile removal is executed. This can be a local or remote machine. + +.PARAMETER SID +Specifies the Security Identifier (SID) of the user profile to remove. This parameter accepts pipeline input, allowing multiple SIDs to be processed sequentially. + +.PARAMETER RegistryPath +Specifies the registry path where user profile information is stored. For example, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList`. + +.PARAMETER ProfileFolderPath +Specifies the folder path where user profile data is stored. For example, `C:\Users`. + +.PARAMETER RegistryHive +Specifies the registry hive (e.g., HKLM for HKEY_LOCAL_MACHINE or HKCU for HKEY_CURRENT_USER) under which the profile keys are located. + +.PARAMETER Force +Forces the removal of profiles without prompting for confirmation. When this switch is used, profiles are removed without any user interaction. + +.PARAMETER AuditOnly +Performs an audit without removing any profiles. The audit results are output to the pipeline, and no changes are made to the registry. + +.PARAMETER Confirm +If specified, the user is prompted for confirmation before removing each profile. The prompt is skipped if `Force` or `AuditOnly` switches are used. + +.EXAMPLE +Get-UserProfiles | Invoke-UserProfileRegRemoval -ComputerName 'Server01' -RegistryPath 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' -Force + +Description: +Removes all user profiles from the registry on Server01 without prompting for confirmation, as the `Force` switch is used. + +.EXAMPLE +Get-UserProfiles | Invoke-UserProfileRegRemoval -ComputerName 'Server02' -RegistryPath 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' -AuditOnly + +Description: +Performs an audit of the user profiles on Server02, but does not remove any profiles. The audit results are output to the pipeline. + +.EXAMPLE +'S-1-5-21-12345' | Invoke-UserProfileRegRemoval -ComputerName 'Server03' -RegistryPath 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' + +Description: +Processes the specified SID ('S-1-5-21-12345') for removal on Server03. If `Confirm` is specified, the user is prompted before the profile is removed. + +.NOTES +- This function uses pipeline input to process multiple SIDs. +- The function handles both local and remote computers. +- Errors during registry key access or audit failure result in early termination. +- If special system profiles are detected during the audit, they can be skipped based on the implementation of the audit function. +#> +function Invoke-UserProfileRegRemoval +{ + [CmdletBinding()] + param ( + [string]$ComputerName, + + # Accept pipeline input for each SID + [Parameter(ValueFromPipeline = $true)] + [string]$SID, + + [string]$RegistryPath, + [string]$ProfileFolderPath, + [Microsoft.Win32.RegistryHive]$RegistryHive, + [switch]$Force, + [switch]$AuditOnly, + [bool]$Confirm + ) + + Begin + { + # Initialize a flag to determine if processing should continue + $continueProcessing = $true + + try + { + # Try to open the registry key + $BaseKey = Open-RegistryKey -ComputerName $ComputerName -RegistryHive $RegistryHive -RegistryPath $RegistryPath -ErrorAction SilentlyContinue + + # Check if the registry key is valid + if (-not $BaseKey) + { + throw "Failed to open registry key on computer $ComputerName" + } + + # Perform the audit if the BaseKey is valid + $userProfileAudit = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath -IgnoreSpecial + + if (-not $userProfileAudit) + { + throw "Failed to audit user profiles on computer $ComputerName" + } + + } + catch + { + # Catch any exceptions that occur during the process + Write-Error $_.Exception.Message + $continueProcessing = $false # Set the flag to prevent further processing + return # Exit the function early if an error occurs + } + } + + Process + { + # Only proceed if the flag allows processing + if ($continueProcessing) + { + # Process each SID as it flows through the pipeline + $SelectedProfile = Resolve-UserProfileForDeletion -SID $SID -AuditResults $userProfileAudit -ComputerName $ComputerName + + if ($SelectedProfile -is [ProfileDeletionResult]) + { + # Output the ProfileDeletionResult directly to the pipeline + $SelectedProfile + } + else + { + # Skip confirmation if AuditOnly is used + if (-not $AuditOnly) + { + if (-not $Force -and (ShouldContinueWrapper -Context $PSCmdlet -QueryMessage "Do you want to delete SID $SID from $($SelectedProfile.ComputerName)?" -CaptionMessage "Confirm Deletion")) + { + $result = Remove-UserProfileRegistryEntry -SelectedProfile $SelectedProfile -BaseKey $BaseKey -AuditOnly:$AuditOnly + $result + } + elseif ($Force) + { + $result = Remove-UserProfileRegistryEntry -SelectedProfile $SelectedProfile -BaseKey $BaseKey -AuditOnly:$AuditOnly + $result + } + } + else + { + # Just process without confirmation + $result = Remove-UserProfileRegistryEntry -SelectedProfile $SelectedProfile -BaseKey $BaseKey -AuditOnly:$AuditOnly + $result + } + } + } + } + + End + { + # Clean up resources + if ($BaseKey) + { + $BaseKey.Dispose() + } + } +} diff --git a/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 b/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 new file mode 100644 index 0000000..fb222c8 --- /dev/null +++ b/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 @@ -0,0 +1,72 @@ +<# +.SYNOPSIS +Creates a new `ProfileDeletionResult` object with details of a user profile deletion. + +.DESCRIPTION +The `New-ProfileDeletionResult` function generates a new object representing the outcome of a user profile deletion operation. This object can include details such as the SID, profile path, deletion status, and computer name. + +.PARAMETER SID +Specifies the Security Identifier (SID) of the user profile. + +.PARAMETER ProfilePath +Specifies the path to the user profile that was deleted (optional). + +.PARAMETER DeletionSuccess +Specifies whether the profile deletion was successful. + +.PARAMETER DeletionMessage +Provides a message regarding the profile deletion result. + +.PARAMETER ComputerName +Specifies the name of the computer from which the profile was removed. + +.EXAMPLE +New-ProfileDeletionResult -SID 'S-1-5-21-...' -DeletionSuccess $true -DeletionMessage 'Profile removed successfully.' + +Description: +Creates a `ProfileDeletionResult` object indicating that the profile for the specified SID was successfully removed. + +.OUTPUTS +ProfileDeletionResult object containing the details of the deletion operation. +#> +function New-ProfileDeletionResult +{ + [CmdletBinding(DefaultParameterSetName = 'Minimal')] + param ( + # SID is mandatory in all parameter sets + [Parameter(Mandatory = $true, ParameterSetName = 'Full')] + [Parameter(Mandatory = $true, ParameterSetName = 'SuccessOnly')] + [Parameter(Mandatory = $true, ParameterSetName = 'Minimal')] + [string]$SID, + + # Full parameter set properties + [Parameter(Mandatory = $false, ParameterSetName = 'Full')] + [string]$ProfilePath =$null, + + [Parameter(Mandatory = $true, ParameterSetName = 'Full')] + [Parameter(Mandatory = $true, ParameterSetName = 'SuccessOnly')] + [bool]$DeletionSuccess, + + [Parameter(Mandatory = $true, ParameterSetName = 'Full')] + [string]$DeletionMessage, + + [Parameter(Mandatory = $true, ParameterSetName = 'Full')] + [string]$ComputerName + ) + + switch ($PSCmdlet.ParameterSetName) + { + 'Full' + { + return [ProfileDeletionResult]::new($SID, $ProfilePath, $DeletionSuccess, $DeletionMessage, $ComputerName) + } + 'SuccessOnly' + { + return [ProfileDeletionResult]::new($SID, $DeletionSuccess) + } + 'Minimal' + { + return [ProfileDeletionResult]::new($SID) + } + } +} diff --git a/source/Private/RemoveProfileReg/PromptForConfirmation.ps1 b/source/Private/RemoveProfileReg/PromptForConfirmation.ps1 new file mode 100644 index 0000000..dbf5fae --- /dev/null +++ b/source/Private/RemoveProfileReg/PromptForConfirmation.ps1 @@ -0,0 +1,67 @@ +<# +.SYNOPSIS +Prompts the user for confirmation before proceeding with a deletion operation. + +.DESCRIPTION +The `PromptForConfirmation` function asks the user to confirm before performing a deletion operation on a specified computer's registry. If the `AuditOnly` flag is specified, the prompt is skipped. If `Confirm` is set to `$true`, the function displays a confirmation message with details about the number of items to delete and the target computer. The user response is handled by the `ShouldContinueWrapper` function, which manages the confirmation prompt. + +.PARAMETER ComputerName +Specifies the name of the computer where the deletion operation will take place. + +.PARAMETER ItemCount +Specifies the number of profiles to delete from the computer's registry. This is displayed in the confirmation message. + +.PARAMETER AuditOnly +If this switch is specified, the function will skip the confirmation prompt and proceed without making any changes. This is typically used for audit or dry-run scenarios. + +.PARAMETER Confirm +If this switch is specified, the function will always prompt the user for confirmation before proceeding. + +.PARAMETER context +Specifies the execution context, typically used to access methods like `ShouldContinue` for the confirmation prompt. + +.EXAMPLE +PromptForConfirmation -ComputerName 'Server01' -ItemCount 5 -Confirm + +Description: +Prompts the user to confirm the deletion of 5 profiles from the registry of 'Server01'. If the user confirms, the function returns `$true`; otherwise, it returns `$false`. + +.EXAMPLE +PromptForConfirmation -ComputerName 'Server02' -ItemCount 10 -AuditOnly + +Description: +Skips the confirmation prompt since the `AuditOnly` switch is used, and returns `$true` to proceed with the audit operation. + +.NOTES +The function assumes that `ShouldContinueWrapper` is available to handle the actual confirmation prompt. +#> + +function PromptForConfirmation +{ + param ( + [string]$ComputerName, + [int]$ItemCount, + [switch]$AuditOnly, + [switch]$Confirm, + $context + ) + + # Skip prompt if in AuditOnly mode + if ($AuditOnly) + { + return $true + } + + + # Always prompt unless Force is specified or Confirm is explicitly set to false + if ($Confirm -eq $true) + { + $QueryMessage = "Are you sure you want to delete $ItemCount profiles from $ComputerName's registry?" + $CaptionMessage = "Confirm Deletion" + + # Use the ShouldContinueWrapper to handle the prompt + return (ShouldContinueWrapper -Context $context -QueryMessage $QueryMessage -CaptionMessage $CaptionMessage) + } + + return $true # Proceed if Force is used or if AuditOnly is true +} diff --git a/source/Private/RemoveProfileReg/Remove-ProfileRegistryKey.ps1 b/source/Private/RemoveProfileReg/Remove-ProfileRegistryKey.ps1 new file mode 100644 index 0000000..69665d5 --- /dev/null +++ b/source/Private/RemoveProfileReg/Remove-ProfileRegistryKey.ps1 @@ -0,0 +1,41 @@ +<# +.SYNOPSIS +Removes a registry key associated with a specific SID. + +.DESCRIPTION +The `Remove-ProfileRegistryKey` function deletes the registry key associated with a specified SID. If the operation fails, an error is logged. + +.PARAMETER SID +Specifies the Security Identifier (SID) whose registry key is being removed. + +.PARAMETER BaseKey +Specifies the base registry key under which the SID subkey exists. + +.EXAMPLE +Remove-ProfileRegistryKey -SID 'S-1-5-21-...' -BaseKey $RegistryKey + +Description: +Removes the registry key for the specified SID from the provided base key. + +.OUTPUTS +Boolean indicating whether the registry key was successfully removed. +#> + +function Remove-ProfileRegistryKey +{ + param ( + [string]$SID, + [Microsoft.Win32.RegistryKey]$BaseKey + ) + + try + { + Remove-RegistrySubKey -ParentKey $BaseKey -SubKeyName $SID -ThrowOnMissingSubKey $false -Confirm:$false + return $true + } + catch + { + Write-Error "Error removing registry key for SID $SID`: $_" + return $false + } +} diff --git a/source/Private/RemoveProfileReg/Remove-UserProfileRegistryEntry.ps1 b/source/Private/RemoveProfileReg/Remove-UserProfileRegistryEntry.ps1 new file mode 100644 index 0000000..020732d --- /dev/null +++ b/source/Private/RemoveProfileReg/Remove-UserProfileRegistryEntry.ps1 @@ -0,0 +1,98 @@ +<# +.SYNOPSIS +Removes a user profile registry entry and backs up the registry data before deletion. + +.DESCRIPTION +The `Remove-UserProfileRegistryEntry` function removes a user profile from the Windows registry. Before removal, it backs up the registry data to a specified directory. The function also supports audit mode, where no deletion occurs but an audit log is created. + +.PARAMETER SelectedProfile +Specifies the user profile object representing the profile to be deleted. + +.PARAMETER BaseKey +Specifies the base registry key under which the profile's SID subkey exists. + +.PARAMETER AuditOnly +If specified, the function will only perform an audit and will not delete the registry entry. + +.EXAMPLE +Remove-UserProfileRegistryEntry -SelectedProfile $Profile -BaseKey $RegistryKey -AuditOnly + +Description: +Performs an audit of the profile without deleting it from the registry. + +.OUTPUTS +ProfileDeletionResult object indicating the outcome of the deletion or audit operation. +#> + +function Remove-UserProfileRegistryEntry +{ + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param ( + [Parameter(ValueFromPipelineByPropertyName = $true)] + [UserProfile]$SelectedProfile, + [Microsoft.Win32.RegistryKey]$BaseKey, + [switch]$AuditOnly + ) + + Process + { + # Prepare the deletion result parameters + $deletionResultParams = @{ + SID = $SelectedProfile.SID + ProfilePath = $SelectedProfile.ProfilePath + ComputerName = $SelectedProfile.ComputerName + DeletionSuccess = $false + DeletionMessage = "Profile not removed." + } + + # Check if BaseKey is null + if (-not $BaseKey) + { + $deletionResultParams.DeletionMessage = "Failed: BaseKey is null, cannot remove the profile." + New-ProfileDeletionResult @deletionResultParams + return # Return early to stop further processing + } + + # If in audit mode, output an audit-only result directly to the pipeline and return + if ($AuditOnly) + { + $deletionResultParams.DeletionSuccess = $true + $deletionResultParams.DeletionMessage = "Audit only, no deletion performed." + New-ProfileDeletionResult @deletionResultParams + return # Return to allow pipeline to continue with the next item + } + + # Determine backup directory + $RegBackUpDirectory = Get-DirectoryPath -basePath $env:WinProfileOps_RegBackUpDirectory -ComputerName $SelectedProfile.ComputerName -IsLocal ($SelectedProfile.ComputerName -eq $env:COMPUTERNAME) + + # Backup the registry key, output failure message if backup fails and skip further processing + if (-not (Backup-RegistryKeyForSID -SID $SelectedProfile.SID -BaseKey $BaseKey -RegBackUpDirectory $RegBackUpDirectory -ComputerName $SelectedProfile.ComputerName)) + { + $deletionResultParams.DeletionMessage = "Failed to backup profile." + New-ProfileDeletionResult @deletionResultParams + return # Return to allow pipeline to continue with the next item + } + + # Remove the registry key, output failure message if removal fails + if (-not (Remove-ProfileRegistryKey -SID $SelectedProfile.SID -BaseKey $BaseKey)) + { + $deletionResultParams.DeletionMessage = "Failed to remove profile registry key." + New-ProfileDeletionResult @deletionResultParams + return # Return to allow pipeline to continue with the next item + } + + # Verify the removal and update the result + if (Confirm-ProfileRemoval -SID $SelectedProfile.SID -BaseKey $BaseKey) + { + $deletionResultParams.DeletionSuccess = $true + $deletionResultParams.DeletionMessage = "Profile removed successfully." + } + else + { + $deletionResultParams.DeletionMessage = "Profile removal verification failed." + } + + # Output the final deletion result + New-ProfileDeletionResult @deletionResultParams + } +} diff --git a/source/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.ps1 b/source/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.ps1 new file mode 100644 index 0000000..560b232 --- /dev/null +++ b/source/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.ps1 @@ -0,0 +1,61 @@ +<# +.SYNOPSIS +Finds the user profile for a specific SID in an audit result. + +.DESCRIPTION +The `Resolve-UserProfileForDeletion` function searches through audit results to find the profile associated with a given SID. If the profile is not found, a warning is logged, and a `ProfileDeletionResult` is returned indicating failure. + +.PARAMETER SID +Specifies the Security Identifier (SID) of the profile to search for. + +.PARAMETER AuditResults +Specifies the audit results to search for the profile. + +.PARAMETER ComputerName +Specifies the name of the computer where the profile is located. + +.EXAMPLE +Resolve-UserProfileForDeletion -SID 'S-1-5-21-...' -AuditResults $AuditResults -ComputerName 'Server01' + +Description: +Finds the user profile associated with the specified SID in the audit results for Server01. + +.OUTPUTS +UserProfile or ProfileDeletionResult object. +#> +function Resolve-UserProfileForDeletion +{ + param ( + [Parameter(Mandatory = $true)] + [string]$SID, # The SID to search for + [Parameter(Mandatory = $false)] + [UserProfile[]]$AuditResults, # The audit results + [Parameter(Mandatory = $true)] + [string]$ComputerName # The target computer name + ) + + # Find the corresponding user profile from the audit + $SelectedProfile = $AuditResults | Where-Object { $_.SID -eq $SID } + + # Handle cases where profile is not found + if ($null -eq $SelectedProfile) + { + # Determine if it's an invalid SID or just not found + $message = if (Validate-SIDFormat -SID $SID) + { + "Profile not found" + Write-Warning "Profile not found for SID: $SID on $ComputerName." + } + else + { + "Invalid SID format encountered" + Write-Warning "Invalid SID format encountered: $SID on $ComputerName." + } + + # Return a ProfileDeletionResult if the profile is not found or invalid + return New-ProfileDeletionResult -SID $SID -ProfilePath $null -DeletionSuccess $false -DeletionMessage $message -ComputerName $ComputerName + } + + # If profile is found, return the UserProfile object + return $SelectedProfile +} diff --git a/source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 b/source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 new file mode 100644 index 0000000..e5c116d --- /dev/null +++ b/source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 @@ -0,0 +1,45 @@ +<# +.SYNOPSIS +Resolves a list of usernames to their corresponding Security Identifiers (SIDs). + +.DESCRIPTION +The `Resolve-UsernamesToSIDs` function resolves each provided username to its corresponding SID on the specified computer. If a username cannot be resolved, a warning is logged. + +.PARAMETER Usernames +Specifies an array of usernames to resolve to SIDs. + +.PARAMETER ComputerName +Specifies the name of the computer on which to resolve the usernames. + +.EXAMPLE +Resolve-UsernamesToSIDs -Usernames 'user1', 'user2' -ComputerName 'Server01' + +Description: +Resolves the SIDs for 'user1' and 'user2' on Server01. + +.OUTPUTS +Array of SIDs corresponding to the provided usernames. +#> + +function Resolve-UsernamesToSIDs +{ + param ( + [string[]]$Usernames, + [string]$ComputerName + ) + + $SIDs = @() + foreach ($Username in $Usernames) + { + $SID = Get-SIDFromUsername -Username $Username -ComputerName $ComputerName + if ($SID) + { + $SIDs += $SID + } + else + { + Write-Warning "Could not resolve SID for username $Username on $ComputerName." + } + } + return $SIDs +} diff --git a/source/Private/UserProfileAudit/Process-RegistryProfiles.ps1 b/source/Private/UserProfileAudit/Process-RegistryProfiles.ps1 index cde53b1..f5d1bb5 100644 --- a/source/Private/UserProfileAudit/Process-RegistryProfiles.ps1 +++ b/source/Private/UserProfileAudit/Process-RegistryProfiles.ps1 @@ -50,9 +50,22 @@ function Process-RegistryProfiles foreach ($regProfile in $RegistryProfiles) { $profilePath = $regProfile.ProfilePath - $folderExists = $null + $folderExists = $false $accessError = $false + if ($null -eq $profilePath) + { + $isSpecial = Test-SpecialAccount -SID $regProfile.SID + $userProfile = Test-OrphanedProfile -SID $regProfile.SID -ProfilePath $null ` + -FolderExists $folderExists -AccessError $accessError -IgnoreSpecial $IgnoreSpecial ` + -IsSpecial $isSpecial -ComputerName $ComputerName + + # Add this line to include the user profile in the processed array + $processedProfiles += $userProfile + continue + } + + $folderName = Split-Path -Path $profilePath -Leaf $isSpecial = Test-SpecialAccount -FolderName $folderName -SID $regProfile.SID -ProfilePath $profilePath if ($IgnoreSpecial -and $isSpecial) @@ -75,7 +88,7 @@ function Process-RegistryProfiles Write-Warning "Error testing folder existence for profile: $profilePath. Error: $_" } - $folderName = Split-Path -Path $profilePath -Leaf + $userProfile = Test-OrphanedProfile -SID $regProfile.SID -ProfilePath $profilePath ` -FolderExists $folderExists -AccessError $accessError -IgnoreSpecial $IgnoreSpecial ` diff --git a/source/Private/Test-FolderExists.ps1 b/source/Private/ValidateFunctions/Test-FolderExists.ps1 similarity index 100% rename from source/Private/Test-FolderExists.ps1 rename to source/Private/ValidateFunctions/Test-FolderExists.ps1 diff --git a/source/Private/Test-OrphanedProfile.ps1 b/source/Private/ValidateFunctions/Test-OrphanedProfile.ps1 similarity index 89% rename from source/Private/Test-OrphanedProfile.ps1 rename to source/Private/ValidateFunctions/Test-OrphanedProfile.ps1 index bcf1b55..4db3fd1 100644 --- a/source/Private/Test-OrphanedProfile.ps1 +++ b/source/Private/ValidateFunctions/Test-OrphanedProfile.ps1 @@ -48,6 +48,11 @@ function Test-OrphanedProfile return New-UserProfileObject -SID $SID -ProfilePath $ProfilePath -IsOrphaned $true ` -OrphanReason "MissingFolder" -ComputerName $ComputerName -IsSpecial $IsSpecial } + elseif (-not $ProfilePath -and -not $FolderExists) + { + return New-UserProfileObject -SID $SID -ProfilePath $null -IsOrphaned $true ` + -OrphanReason "MissingProfileImagePathAndFolder" -ComputerName $ComputerName -IsSpecial $IsSpecial + } else { return New-UserProfileObject -SID $SID -ProfilePath $ProfilePath -IsOrphaned $false ` diff --git a/source/Private/Test-SpecialAccount.ps1 b/source/Private/ValidateFunctions/Test-SpecialAccount.ps1 similarity index 100% rename from source/Private/Test-SpecialAccount.ps1 rename to source/Private/ValidateFunctions/Test-SpecialAccount.ps1 diff --git a/source/Private/Validate-SIDFormat.ps1 b/source/Private/ValidateFunctions/Validate-SIDFormat.ps1 similarity index 100% rename from source/Private/Validate-SIDFormat.ps1 rename to source/Private/ValidateFunctions/Validate-SIDFormat.ps1 diff --git a/source/Public/Get-OrphanedProfiles.ps1 b/source/Public/Get-OrphanedProfiles.ps1 index 72dcd1e..f5b5656 100644 --- a/source/Public/Get-OrphanedProfiles.ps1 +++ b/source/Public/Get-OrphanedProfiles.ps1 @@ -33,7 +33,7 @@ function Get-OrphanedProfiles [OutputType([UserProfile[]])] param ( [string]$ComputerName = $env:COMPUTERNAME, - [string]$ProfileFolderPath = "$env:SystemDrive\Users", + [string]$ProfileFolderPath = $env:WinProfileOps_ProfileFolderPath, [switch]$IgnoreSpecial ) diff --git a/source/Public/Get-UserProfilesFromFolders.ps1 b/source/Public/Get-UserProfilesFromFolders.ps1 index 844f566..4f6793f 100644 --- a/source/Public/Get-UserProfilesFromFolders.ps1 +++ b/source/Public/Get-UserProfilesFromFolders.ps1 @@ -35,7 +35,7 @@ function Get-UserProfilesFromFolders [CmdletBinding()] param ( [string]$ComputerName = $env:COMPUTERNAME, - [string]$ProfileFolderPath = "$env:SystemDrive\Users" + [string]$ProfileFolderPath = $env:WinProfileOps_ProfileFolderPath ) try diff --git a/source/Public/Invoke-UserProfileAudit.ps1 b/source/Public/Invoke-UserProfileAudit.ps1 index 05f85a7..8cf8a6d 100644 --- a/source/Public/Invoke-UserProfileAudit.ps1 +++ b/source/Public/Invoke-UserProfileAudit.ps1 @@ -53,13 +53,17 @@ function Invoke-UserProfileAudit [Parameter(Mandatory = $false, ValueFromPipeline = $true)] [string]$ComputerName = $env:COMPUTERNAME, - [string]$ProfileFolderPath = "$env:SystemDrive\Users", + [string]$ProfileFolderPath = $env:WinProfileOps_ProfileFolderPath, [switch]$IgnoreSpecial ) begin { $AllProfiles = @() + if ($null -eq $ComputerName) + { + $ComputerName = $env:COMPUTERNAME + } } process diff --git a/source/Public/Remove-UserProfilesFromRegistry.ps1 b/source/Public/Remove-UserProfilesFromRegistry.ps1 new file mode 100644 index 0000000..7a031f7 --- /dev/null +++ b/source/Public/Remove-UserProfilesFromRegistry.ps1 @@ -0,0 +1,176 @@ +<# +.SYNOPSIS +Removes user profiles from the Windows registry based on SIDs, Usernames, or UserProfile objects. + +.DESCRIPTION +The Remove-UserProfilesFromRegistry function allows you to remove user profiles from the Windows registry. +It supports three parameter sets: UserProfileSet, SIDSet, and UserNameSet. The function can be used in +audit-only mode, where no actual removal is performed, or in deletion mode where profiles are removed. + +If AuditOnly is specified, the function will simply output the profiles to be removed without actually performing +any deletions. The function can prompt for confirmation before deletion if required, or use the Force switch +to bypass confirmation. + +.PARAMETER UserProfiles +An array of UserProfile objects to remove from the registry. This parameter is mandatory in the "UserProfileSet" +parameter set. UserProfiles should include the necessary information such as SID, ProfilePath, and ComputerName. + +.PARAMETER SIDs +An array of SIDs of user profiles to remove from the registry. This parameter is mandatory in the "SIDSet" +parameter set. + +.PARAMETER Usernames +An array of usernames to resolve into SIDs and remove from the registry. This parameter is mandatory in the +"UserNameSet" parameter set. + +.PARAMETER ComputerName +Specifies the computer name from which the user profiles should be removed. If not provided, it defaults to +the local computer. + +.PARAMETER AuditOnly +When specified, the function only audits the user profiles and does not perform actual deletion. It will output +information about the profiles that would have been removed. + +.PARAMETER Force +Forces the removal of the user profiles without prompting for confirmation. + +.Outputs +ProfileDeletionResult objects that contain information about the deletion results. + +.EXAMPLE +Remove-UserProfilesFromRegistry -SIDs "S-1-5-21-1234567890-1", "S-1-5-21-1234567890-2" + +Removes user profiles associated with the provided SIDs from the registry of the local computer. + +.EXAMPLE +Remove-UserProfilesFromRegistry -Usernames "john.doe", "jane.smith" -ComputerName "SERVER01" -Force + +Removes the profiles associated with the specified usernames on the "SERVER01" machine without prompting for confirmation. + +.EXAMPLE +Remove-UserProfilesFromRegistry -UserProfiles $userProfileList -AuditOnly + +Audits the profiles in the $userProfileList and outputs what would have been removed without performing actual deletions. + +.NOTES +Requires administrative privileges to remove profiles from the registry. + +.LINK +Get-Help about_Registry +Get-Help about_Profiles +#> +function Remove-UserProfilesFromRegistry +{ + [outputType([ProfileDeletionResult])] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "UserProfileSet")] + [UserProfile[]]$UserProfiles, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "SIDSet")] + [string[]]$SIDs, + + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "UserNameSet")] + [string[]]$Usernames, + + [string]$ComputerName = $env:COMPUTERNAME, + [switch]$AuditOnly, + [switch]$Force + # Default confirm behavior to true + ) + + Begin + { + # Retrieve necessary environment variables + $RegistryPath = Test-EnvironmentVariable -Name 'WinProfileOps_RegistryPath' + $ProfileFolderPath = Test-EnvironmentVariable -Name 'WinProfileOps_ProfileFolderPath' + $RegistryHive = Test-EnvironmentVariable -Name 'WinProfileOps_RegistryHive' + + # Resolve SIDs if Usernames are provided + if ($PSCmdlet.ParameterSetName -eq 'UserNameSet') + { + $SIDs = Resolve-UsernamesToSIDs -Usernames $Usernames -ComputerName $ComputerName + + # If no SIDs were resolved, return early + if (-not $SIDs) + { + Write-Error "No SIDs could be resolved for the provided usernames." + return + } + } + + # Group UserProfiles by computer name if using UserProfileSet + if ($PSCmdlet.ParameterSetName -eq 'UserProfileSet') + { + $profilesByComputer = $UserProfiles | Group-Object -Property ComputerName + } + + # Handle confirmation: default behavior should be prompting unless explicitly set to false + $Confirm = if ($PSCmdlet.MyInvocation.BoundParameters.ContainsKey('Confirm')) + { + $PSCmdlet.MyInvocation.BoundParameters['Confirm'] + } + else + { + $true # Default to true, always prompt unless explicitly overridden + } + } + Process + { + # Process UserProfileSet - prompt per computer + if ($PSCmdlet.ParameterSetName -eq 'UserProfileSet') + { + foreach ($profileGroup in $profilesByComputer) + { + $thisComputerName = $profileGroup.Name + $SIDs = $profileGroup.Group.GetEnumerator().SID + $profileCount = $profileGroup.Count + + try + { + # Call the confirmation prompt and skip this group if the user does not confirm + if (-not (PromptForConfirmation -ComputerName $thisComputerName -ItemCount $profileCount -AuditOnly:$AuditOnly -Context $PSCmdlet -confirm:$Confirm)) + { + Write-Verbose "User chose not to continue for $thisComputerName, skipping." + continue + } + + # Process the profiles for this computer + $SIDs | Invoke-UserProfileRegRemoval -ComputerName $thisComputerName ` + -RegistryPath $RegistryPath -ProfileFolderPath $ProfileFolderPath ` + -RegistryHive $RegistryHive -Force:$Force -AuditOnly:$AuditOnly -Confirm:$Confirm + } + catch + { + # Handle any errors that occur during processing of this computer + Write-Error "Failed to process $thisComputerName. Error: $_.Exception.Message" + continue # Move to the next computer in the loop + } + } + } + + # Process SIDSet and UserNameSet - prompt once for the given computer name + if ($PSCmdlet.ParameterSetName -eq 'SIDSet' -or $PSCmdlet.ParameterSetName -eq 'UserNameSet') + { + $itemCount = $SIDs.Count + + # Call the confirmation prompt and stop if the user does not confirm + if (-not (PromptForConfirmation -ComputerName $ComputerName -ItemCount $itemCount -AuditOnly:$AuditOnly -Context $PSCmdlet -confirm:$Confirm)) + { + Write-Verbose "User chose not to continue for $thisComputerName, skipping." + return + } + + # Process the SIDs for this computer name + $SIDs | Invoke-UserProfileRegRemoval -ComputerName $ComputerName ` + -RegistryPath $RegistryPath -ProfileFolderPath $ProfileFolderPath ` + -RegistryHive $RegistryHive -Force:$Force -AuditOnly:$AuditOnly -Confirm:$Confirm + } + } + + End + { + # No need to manually return results; PowerShell will output naturally + } +} diff --git a/source/prefix.ps1 b/source/prefix.ps1 index 38443f8..d76534e 100644 --- a/source/prefix.ps1 +++ b/source/prefix.ps1 @@ -1,28 +1,23 @@ # Your functions - # Check if the current user is an administrator $windowsIdentity = [Security.Principal.WindowsIdentity]::GetCurrent() $windowsPrincipal = New-Object Security.Principal.WindowsPrincipal($windowsIdentity) -$isAdmin = $windowsPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) - -# Set the environment variable based on whether the user is an admin -if ($isAdmin) -{ - $ENV:WinProfileOps_IsAdmin = $true -} -else -{ - $ENV:WinProfileOps_IsAdmin = $false -} +$env:WinProfileOps_IsAdmin = $windowsPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +$env:WinProfileOps_RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" +$env:WinProfileOps_RegistryHive = [Microsoft.Win32.RegistryHive]::LocalMachine +$env:WinProfileOps_RegBackUpDirectory = "C:\LHStuff\RegBackUp" +$env:WinProfileOps_ProfileFolderPath = $env:SystemDrive + "\Users" -Write-Verbose "User is an administrator: $ENV:WinProfileOps_IsAdmin" [scriptblock]$SB = { if (Test-Path Env:\WinProfileOps_IsAdmin) { - Remove-Item Env:\WinProfileOps_IsAdmin - Write-Verbose "WinProfileOps: Removed WinProfileOps_IsAdmin environment variable." + Remove-Item Env:\WinProfileOps_IsAdmin -errorAction SilentlyContinue + Remove-Item Env:\WinProfileOps_RegistryPath -ErrorAction SilentlyContinue + Remove-Item Env:\WinProfileOps_RegistryHive -ErrorAction SilentlyContinue + Remove-Item Env:\WinProfileOps_RegBackUpDirectory -ErrorAction SilentlyContinue + Remove-Item Env:\WinProfileOps_ProfileFolderPath -ErrorAction SilentlyContinue } } diff --git a/tests/Helpers/MockedProfListReg.ps1 b/tests/Helpers/MockedProfListReg.ps1 new file mode 100644 index 0000000..2c0da97 --- /dev/null +++ b/tests/Helpers/MockedProfListReg.ps1 @@ -0,0 +1,67 @@ +Remove-Module WinProfileOps -ErrorAction SilentlyContinue +Import-Module "D:\1_Code\GithubRepos\WinProfileOps\output\module\WinProfileOps\0.0.1\WinProfileOps.psd1" -Force + + +$null = Remove-Item -Path $env:WinProfileOps_RegBackUpDirectory -Recurse -Force -ErrorAction SilentlyContinue + +$TestDrive = "C:\Temp\TestDirectory" + +if (Test-Path "$TestDrive\Users") +{ + $null = Remove-Item -Path "$TestDrive\Users" -Recurse -Force -ErrorAction SilentlyContinue +} + +# Create mock profile folders in TestDrive +$MockProfilePath = mkdir "$TestDrive\Users" +$MockUsers = @( + @{ + Foldername = "User1" + SID = "S-1-5-21-1234567890-1" + }, + @{ + Foldername = "User2" + SID = "S-1-5-21-1234567890-2" + }, + @{ + Foldername = "User3" + SID = "S-1-5-21-1234567890-3" + } +) + +$MockUsers | ForEach-Object { + $null = mkdir "$TestDrive\Users\$($_.Foldername)" +} + +# Mock registry entries in TestRegistry +$MockRegistryPath = "HKCU:\Software\Pester\ProfileList" + +# Create registry path if it doesn't exist +if (-not (Test-Path $MockRegistryPath)) +{ + $null = New-Item -Path $MockRegistryPath -ItemType Directory +} + +# Set up the environment variable for the registry path +$env:WinProfileOps_RegistryPath = "Software\Pester\ProfileList" +$env:WinProfileOps_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser +$env:WinProfileOps_ProfileFolderPath = "$TestDrive\Users" + +# Create registry items for each mock user +$MockUsers | ForEach-Object { + $SID = $_.SID + $FolderName = $_.Foldername + $RegistryItemPath = "$MockRegistryPath\$SID" + + # Create registry key and set profile path + if (-not (Test-Path $RegistryItemPath)) + { + $null = New-Item -Path $RegistryItemPath + } + + $null = Set-ItemProperty -Path $RegistryItemPath -Name ProfileImagePath -Value "$TestDrive\Users\$FolderName" +} + +$ProfileFolderPath = $env:WinProfileOps_ProfileFolderPath +$userProfileAudit = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath -IgnoreSpecial + +$out = Remove-UserProfilesFromRegistry -SIDs "S-1-5-21-1234567890-1", "S-1-5-21-1234567890-2" -Confirm:$false diff --git a/tests/Intergration/PublicFunctions.tests.ps1 b/tests/Intergration/PublicFunctions.tests.ps1 new file mode 100644 index 0000000..66cbf28 --- /dev/null +++ b/tests/Intergration/PublicFunctions.tests.ps1 @@ -0,0 +1,675 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + # Import the module being tested + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName + + + +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force + + # Clean up environment variables + Remove-Item -Path Env:Registry_Path -ErrorAction SilentlyContinue + Remove-Item -Path Env:Export_Path -ErrorAction SilentlyContinue + +} + +Describe "PublicFuntions Tests" -Tag "Intergration" { + + Context "Get-UserProfilesFromFolders" { + + BeforeEach { + $MockOutProfilePath = mkdir "$TestDrive\Users" + $MockeFolderNames = @("User1", "User2", "User3") + + $MockedItems = $MockeFolderNames | ForEach-Object { + $FolderName = $_ + mkdir "$TestDrive\Users\$folderName" + } + } + + AfterEach { + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force + } + } + + It "Should return an array of user profile folders" { + + #$MockRegPath = "TestRegistry:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" + #$MockOutRegPath = New-Item -Path $MockRegPath + + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $result = Get-UserProfilesFromFolders -ComputerName $ComputerName -ProfileFolderPath $profilePath + + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 3 + $result[0].ProfilePath | Should -Be "$ProfilePath\User1" + } + } + + Context "Get-UserProfilesFromRegistry" { + + BeforeEach { + # Ensure clean-up of TestDrive before creating folders + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force -ErrorAction SilentlyContinue + } + + # Create mock profile folders in TestDrive + $MockProfilePath = mkdir "$TestDrive\Users" + $MockUsers = @( + @{ + Foldername = "User1" + SID = "S-1-5-21-1234567890-1" + }, + @{ + Foldername = "User2" + SID = "S-1-5-21-1234567890-2" + }, + @{ + Foldername = "User3" + SID = "S-1-5-21-1234567890-3" + } + ) + + $MockUsers | ForEach-Object { + mkdir "$TestDrive\Users\$($_.Foldername)" + } + + # Mock registry entries in TestRegistry + $MockRegistryPath = "HKCU:\Software\Pester\ProfileList" + + # Create registry path if it doesn't exist + if (-not (Test-Path $MockRegistryPath)) + { + New-Item -Path $MockRegistryPath -ItemType Directory + } + + # Set up the environment variable for the registry path + $env:WinProfileOps_RegistryPath = "Software\Pester\ProfileList" + $env:WinProfileOps_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser + + # Create registry items for each mock user + $MockUsers | ForEach-Object { + $SID = $_.SID + $FolderName = $_.Foldername + $RegistryItemPath = "$MockRegistryPath\$SID" + + # Create registry key and set profile path + if (-not (Test-Path $RegistryItemPath)) + { + New-Item -Path $RegistryItemPath + } + + Set-ItemProperty -Path $RegistryItemPath -Name ProfileImagePath -Value "$TestDrive\Users\$FolderName" + } + } + + + AfterEach { + # Clean up mock folders and registry items + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force + } + + if (Test-Path "HKCU:\Software\Pester\ProfileList") + { + Remove-Item -Path "HKCU:\Software\Pester\ProfileList" -Recurse + } + + #resetEnvVariables + Remove-Item -Path Env:WinProfileOps_RegistryPath -ErrorAction SilentlyContinue + Remove-Item -Path Env:WinProfileOps_RegistryHive -ErrorAction SilentlyContinue + } + + It "Should return an array of user profiles from the registry" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 3 + $result[0].ProfilePath | Should -Be "$ProfilePath\User1" + } + + + It "It should return object even if missing ProfileImagePath in Registry" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $null = Set-ItemProperty -Path "HKCU:\Software\Pester\ProfileList\S-1-5-21-1234567890-1" -Name ProfileImagePath -Value "" + + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Get-UserProfilesFromRegistry $ComputerName + + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 3 + $result[0].profilePath | Should -BeNullOrEmpty + $result[0].ExistsInRegistry | Should -Be $true + } + + } + + Context "Ivoke-UserProfileAudit" { + + BeforeEach { + # Ensure clean-up of TestDrive before creating folders + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force -ErrorAction SilentlyContinue + } + + # Create mock profile folders in TestDrive + $MockProfilePath = mkdir "$TestDrive\Users" + $MockUsers = @( + @{ + Foldername = "User1" + SID = "S-1-5-21-1234567890-1" + }, + @{ + Foldername = "User2" + SID = "S-1-5-21-1234567890-2" + }, + @{ + Foldername = "User3" + SID = "S-1-5-21-1234567890-3" + } + ) + + $MockUsers | ForEach-Object { + mkdir "$TestDrive\Users\$($_.Foldername)" + } + + # Mock registry entries in TestRegistry + $MockRegistryPath = "HKCU:\Software\Pester\ProfileList" + + # Create registry path if it doesn't exist + if (-not (Test-Path $MockRegistryPath)) + { + New-Item -Path $MockRegistryPath -ItemType Directory + } + + # Set up the environment variable for the registry path + $env:WinProfileOps_RegistryPath = "Software\Pester\ProfileList" + $env:WinProfileOps_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser + + # Create registry items for each mock user + $MockUsers | ForEach-Object { + $SID = $_.SID + $FolderName = $_.Foldername + $RegistryItemPath = "$MockRegistryPath\$SID" + + # Create registry key and set profile path + if (-not (Test-Path $RegistryItemPath)) + { + New-Item -Path $RegistryItemPath + } + + Set-ItemProperty -Path $RegistryItemPath -Name ProfileImagePath -Value "$TestDrive\Users\$FolderName" + } + } + + + AfterEach { + # Clean up mock folders and registry items + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force + } + + if (Test-Path "HKCU:\Software\Pester\ProfileList") + { + Remove-Item -Path "HKCU:\Software\Pester\ProfileList" -Recurse + } + + #resetEnvVariables + Remove-Item -Path Env:WinProfileOps_RegistryPath -ErrorAction SilentlyContinue + Remove-Item -Path Env:WinProfileOps_RegistryHive -ErrorAction SilentlyContinue + } + + It "It should return non orphaned Audit Objects" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $profilePath + + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 3 + $result[0].ProfilePath | Should -Be "$ProfilePath\User1" + } + + It "It should return 1 orphaned due to missing folder" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $null = Remove-Item "$profilePath\User1" -Recurse -Force + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $profilePath + + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 3 + $result[0].OrphanReason | Should -Be "MissingFolder" + $result[0].IsOrphaned | Should -Be $true + } + + + It "It should return 1 orphaned due to missing registry entry" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $null = Remove-Item "HKCU:\Software\Pester\ProfileList\S-1-5-21-1234567890-1" -Recurse -Force + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $profilePath + + $selected = $result | Where-Object { $_.ProfilePath -match "User1" } + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 3 + $selected.OrphanReason | Should -Be "MissingRegistryEntry" + $selected.IsOrphaned | Should -Be $true + } + + It "It should return 2 orphaned due to missing ProfileImagePath in Registry" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $null = Set-ItemProperty -Path "HKCU:\Software\Pester\ProfileList\S-1-5-21-1234567890-1" -Name ProfileImagePath -Value "" + + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $profilePath + + + $Orphaned = $result | Where-Object { $_.IsOrphaned -eq $true } + + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 4 + $Orphaned.Count | Should -Be 2 + $Orphaned[0].OrphanReason | Should -Be "MissingProfileImagePath" + $Orphaned[1].OrphanReason | Should -Be "MissingRegistryEntry" + + } + } + + Context "Get-OrphanedProfiles" { + + BeforeEach { + # Ensure clean-up of TestDrive before creating folders + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force -ErrorAction SilentlyContinue + } + + # Create mock profile folders in TestDrive + $MockProfilePath = mkdir "$TestDrive\Users" + $MockUsers = @( + @{ + Foldername = "User1" + SID = "S-1-5-21-1234567890-1" + }, + @{ + Foldername = "User2" + SID = "S-1-5-21-1234567890-2" + }, + @{ + Foldername = "User3" + SID = "S-1-5-21-1234567890-3" + } + ) + + $MockUsers | ForEach-Object { + mkdir "$TestDrive\Users\$($_.Foldername)" + } + + # Mock registry entries in TestRegistry + $MockRegistryPath = "HKCU:\Software\Pester\ProfileList" + + # Create registry path if it doesn't exist + if (-not (Test-Path $MockRegistryPath)) + { + New-Item -Path $MockRegistryPath -ItemType Directory + } + + # Set up the environment variable for the registry path + $env:WinProfileOps_RegistryPath = "Software\Pester\ProfileList" + $env:WinProfileOps_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser + + # Create registry items for each mock user + $MockUsers | ForEach-Object { + $SID = $_.SID + $FolderName = $_.Foldername + $RegistryItemPath = "$MockRegistryPath\$SID" + + # Create registry key and set profile path + if (-not (Test-Path $RegistryItemPath)) + { + New-Item -Path $RegistryItemPath + } + + Set-ItemProperty -Path $RegistryItemPath -Name ProfileImagePath -Value "$TestDrive\Users\$FolderName" + } + } + + AfterEach { + # Clean up mock folders and registry items + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force + } + + if (Test-Path "HKCU:\Software\Pester\ProfileList") + { + Remove-Item -Path "HKCU:\Software\Pester\ProfileList" -Recurse + } + + #resetEnvVariables + Remove-Item -Path Env:WinProfileOps_RegistryPath -ErrorAction SilentlyContinue + Remove-Item -Path Env:WinProfileOps_RegistryHive -ErrorAction SilentlyContinue + } + + It "Should return null if no orphaned profiles are found" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Get-OrphanedProfiles -ComputerName $ComputerName -ProfileFolderPath $profilePath + + # Validate the result + $result | Should -BeNullOrEmpty + $result.Count | Should -Be 0 + } + + It "Should return 1 orphaned profile due to missing folder" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $null = Remove-Item "$profilePath\User1" -Recurse -Force + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Get-OrphanedProfiles -ComputerName $ComputerName -ProfileFolderPath $profilePath + + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 1 + $result.OrphanReason | Should -Be "MissingFolder" + $result.IsOrphaned | Should -Be $true + } + + It "Should return 1 orphaned profile due to missing registry entry" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $null = Remove-Item "HKCU:\Software\Pester\ProfileList\S-1-5-21-1234567890-1" -Recurse -Force + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Get-OrphanedProfiles -ComputerName $ComputerName -ProfileFolderPath $profilePath + + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 1 + $result.OrphanReason | Should -Be "MissingRegistryEntry" + $result.IsOrphaned | Should -Be $true + } + + It "Should return 2 orphaned profile due to missing registry entry and folder" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $null = Remove-Item "HKCU:\Software\Pester\ProfileList\S-1-5-21-1234567890-1" -Recurse -Force + $null = Remove-Item "$profilePath\User2" -Recurse -Force + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Get-OrphanedProfiles -ComputerName $ComputerName -ProfileFolderPath $profilePath + + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 2 + + $result[0].OrphanReason | Should -Be "MissingFolder" + $result[0].IsOrphaned | Should -Be $true + + $result[1].OrphanReason | Should -Be "MissingRegistryEntry" + $result[1].IsOrphaned | Should -Be $true + + } + + } + + Context "Remove-UserProfilesFromRegistry" { + + BeforeEach { + # Ensure clean-up of TestDrive before creating folders + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force -ErrorAction SilentlyContinue + } + + # Create mock profile folders in TestDrive + $MockProfilePath = mkdir "$TestDrive\Users" + $MockUsers = @( + @{ + Foldername = "User1" + SID = "S-1-5-21-1234567890-1" + }, + @{ + Foldername = "User2" + SID = "S-1-5-21-1234567890-2" + }, + @{ + Foldername = "User3" + SID = "S-1-5-21-1234567890-3" + } + ) + + $MockUsers | ForEach-Object { + mkdir "$TestDrive\Users\$($_.Foldername)" + } + + # Mock registry entries in TestRegistry + $MockRegistryPath = "HKCU:\Software\Pester\ProfileList" + + # Create registry path if it doesn't exist + if (-not (Test-Path $MockRegistryPath)) + { + New-Item -Path $MockRegistryPath -ItemType Directory + } + + # Set up the environment variable for the registry path + $env:WinProfileOps_RegistryPath = "Software\Pester\ProfileList" + $env:WinProfileOps_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser + $env:WinProfileOps_ProfileFolderPath = "$TestDrive\Users" + + # Create registry items for each mock user + $MockUsers | ForEach-Object { + $SID = $_.SID + $FolderName = $_.Foldername + $RegistryItemPath = "$MockRegistryPath\$SID" + + # Create registry key and set profile path + if (-not (Test-Path $RegistryItemPath)) + { + New-Item -Path $RegistryItemPath + } + + Set-ItemProperty -Path $RegistryItemPath -Name ProfileImagePath -Value "$TestDrive\Users\$FolderName" + } + } + + AfterEach { + # Clean up mock folders and registry items + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force + } + + if (Test-Path "HKCU:\Software\Pester\ProfileList") + { + Remove-Item -Path "HKCU:\Software\Pester\ProfileList" -Recurse + } + + # Reset environment variables + Remove-Item -Path Env:WinProfileOps_RegistryPath -ErrorAction SilentlyContinue + Remove-Item -Path Env:WinProfileOps_RegistryHive -ErrorAction SilentlyContinue + Remove-Item -Path Env:WinProfileOps_ProfileFolderPath -ErrorAction SilentlyContinue + } + + It "Should remove the specified profiles from the registry" { + $testSID = "S-1-5-21-1234567890-1" + $profilePath = "$TestDrive\Users" + $ComputerName = $Env:COMPUTERNAME + + # Validate the registry entry exists before removal + (Test-Path "HKCU:\Software\Pester\ProfileList\$testSID") | Should -Be $true + + # Call Remove-UserProfilesFromRegistry to remove the profile + $result = Remove-UserProfilesFromRegistry -SIDs $testSID -Force -Confirm:$false + + # Validate that the profile was removed from the registry + (Test-Path "HKCU:\Software\Pester\ProfileList\$testSID") | Should -Be $false + $result.DeletionSuccess | Should -Be $true + } + + It "Should not remove profiles in AuditOnly mode" { + $testSID = "S-1-5-21-1234567890-1" + $profilePath = "$TestDrive\Users" + $ComputerName = $Env:COMPUTERNAME + + # Validate the registry entry exists before attempting audit + (Test-Path "HKCU:\Software\Pester\ProfileList\$testSID") | Should -Be $true + + # Call Remove-UserProfilesFromRegistry with -AuditOnly + $result = Remove-UserProfilesFromRegistry -SIDs $testSID -AuditOnly + + # Validate that the profile was not removed + (Test-Path "HKCU:\Software\Pester\ProfileList\$testSID") | Should -Be $true + $result | Should -Not -BeNullOrEmpty + $result.DeletionMessage | Should -Be "Audit only, no deletion performed." + } + + It "Should handle missing registry entries gracefully" { + $missingSID = "S-1-5-21-1234567890-999" # Non-existing SID + $profilePath = "$TestDrive\Users" + $ComputerName = $Env:COMPUTERNAME + + # Call Remove-UserProfilesFromRegistry on a missing profile + $result = Remove-UserProfilesFromRegistry -SIDs $missingSID -Force -Confirm:$false + + # Validate the result should indicate failure due to missing registry entry + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be "Profile not found" + } + + It "Should remove multiple profiles from the registry" { + $testSIDs = @("S-1-5-21-1234567890-1", "S-1-5-21-1234567890-2") + $profilePath = "$TestDrive\Users" + $ComputerName = $Env:COMPUTERNAME + + # Validate the registry entries exist before removal + $testSIDs | ForEach-Object { + (Test-Path "HKCU:\Software\Pester\ProfileList\$_") | Should -Be $true + } + + # Call Remove-UserProfilesFromRegistry to remove the profiles + $result = Remove-UserProfilesFromRegistry -SIDs $testSIDs -Force -Confirm:$false + + # Validate that the profiles were removed from the registry + $testSIDs | ForEach-Object { + (Test-Path "HKCU:\Software\Pester\ProfileList\$_") | Should -Be $false + } + + $result.DeletionSuccess | ForEach-Object { $_ | Should -Be $true } + } + + It "Should handle profiles with no registry entries" { + $testSID = "S-1-5-21-1234567890-1" + $profilePath = "$TestDrive\Users" + $ComputerName = $Env:COMPUTERNAME + + # Remove registry entry for the test profile + Remove-Item "HKCU:\Software\Pester\ProfileList\$testSID" -Recurse -Force + + # Call Remove-UserProfilesFromRegistry + $result = Remove-UserProfilesFromRegistry -SIDs $testSID -Force -Confirm:$false + + # Validate result indicates no registry entry found + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be "Profile not found" + } + + It "Should handle profiles with missing folders" { + $testSID = "S-1-5-21-1234567890-1" + $profilePath = "$TestDrive\Users" + $ComputerName = $Env:COMPUTERNAME + + # Remove folder for the profile + Remove-Item "$profilePath\User1" -Recurse -Force + + # Call Remove-UserProfilesFromRegistry + $result = Remove-UserProfilesFromRegistry -SIDs $testSID -Force -Confirm:$false + + # Validate the registry entry was removed + (Test-Path "HKCU:\Software\Pester\ProfileList\$testSID") | Should -Be $false + $result.DeletionSuccess | Should -Be $true + } + + It "Should prompt for confirmation before removing profiles" { + $testSID = "S-1-5-21-1234567890-1" + $profilePath = "$TestDrive\Users" + $ComputerName = $Env:COMPUTERNAME + + InModuleScope -ScriptBlock { + mock ShouldContinueWrapper { + $false + } + } + + # Call Remove-UserProfilesFromRegistry without confirming + $result = Remove-UserProfilesFromRegistry -SIDs $testSID -Force -Confirm:$true + + # assert ShouldContinueWrapper was called + Assert-MockCalled ShouldContinueWrapper -Exactly 1 -Scope It + + # Validate that the profile was not removed + (Test-Path "HKCU:\Software\Pester\ProfileList\$testSID") | Should -Be $true + } + + It "Should handel UserProfile Types from the Pipeline" { + $computerName = $Env:COMPUTERNAME + $userProfileAudit = Invoke-UserProfileAudit -ProfileFolderPath $env:WinProfileOps_ProfileFolderPath -IgnoreSpecial + + if ($userProfileAudit.count -eq 3) + { + $result = Remove-UserProfilesFromRegistry -UserProfiles $userProfileAudit -AuditOnly + } + + $result | Should -Not -BeNullOrEmpty + $result.count | Should -Be 3 + $result | ForEach-Object { + $_.DeletionSuccess | Should -Be $true + } + + } + + } +} diff --git a/tests/Unit/Private/Helpers/Get-SIDFromUsername.tests.ps1 b/tests/Unit/Private/Helpers/Get-SIDFromUsername.tests.ps1 new file mode 100644 index 0000000..0641b4c --- /dev/null +++ b/tests/Unit/Private/Helpers/Get-SIDFromUsername.tests.ps1 @@ -0,0 +1,157 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Get-SIDFromUsername' -Tags "Pivate", "Helpers" { + # Mock the Get-CimInstance cmdlet to simulate different scenarios + + Context 'When the username exists and has a valid SID' { + It 'should return the correct SID' { + + InModuleScope -ScriptBlock { + + $ComputerName = 'Server01' + # Mock Get-CimInstance to return a valid SID + Mock -CommandName Get-CimInstance -MockWith { + @{ + SID = 'S-1-5-21-1234567890-1234567890-1234567890-1001' + } + } + + # Act: Call the function + $result = Get-SIDFromUsername -Username 'JohnDoe' -ComputerName $ComputerName + + # Assert: Verify the result is the correct SID + $result | Should -Be 'S-1-5-21-1234567890-1234567890-1234567890-1001' + } + } + } + + Context 'When the username does not exist' { + It 'should return null and show a warning' { + + InModuleScope -ScriptBlock { + $ComputerName = 'Server01' + + # Mock Get-CimInstance to return null (user not found) + Mock -CommandName Get-CimInstance -MockWith { $null } + + # Mock Write-Warning to capture the warning message + Mock -CommandName Write-Warning + + # Act: Call the function + $result = Get-SIDFromUsername -Username 'NonExistentUser' -ComputerName $ComputerName + + # Assert: The result should be null + $result | Should -BeNullOrEmpty + + # Assert: Verify that the warning message was displayed + Assert-MockCalled -CommandName Write-Warning -Exactly 1 -Scope It + } + } + } + + Context 'When an error occurs while querying' { + It 'should return null and display a warning with error information' { + + InModuleScope -ScriptBlock { + $ComputerName = 'Server01' + + # Mock Get-CimInstance to throw an exception + Mock -CommandName Get-CimInstance -MockWith { throw "WMI query failed" } + + # Mock Write-Warning to capture the warning message + Mock -CommandName Write-Warning + + # Act: Call the function + $result = Get-SIDFromUsername -Username 'JohnDoe' -ComputerName $ComputerName + + # Assert: The result should be null + $result | Should -BeNullOrEmpty + + # Assert: Verify that the warning message was displayed + Assert-MockCalled -CommandName Write-Warning -Exactly 1 -Scope It + + } + } + } + + Context 'When mandatory parameters are missing' { + It 'should throw a missing parameter error for Username' { + + InModuleScope -ScriptBlock { + + $ComputerName = 'Server01' + # Act & Assert: Expecting the function to throw an error + { Get-SIDFromUsername -Username $Null -ComputerName $ComputerName } | Should -Throw + + } + } + + It 'should default to localhost when ComputerName is missing' { + InModuleScope -ScriptBlock { + # Mock Get-CimInstance to return a valid SID when queried with 'localhost' + Mock -CommandName Get-CimInstance -MockWith { + @{ + SID = 'S-1-5-21-1234567890-1234567890-1234567890-1001' + } + } + + # Act: Call the function without providing ComputerName + $result = Get-SIDFromUsername -Username 'JohnDoe' + + # Assert: The result should match the mock SID + $result | Should -Be 'S-1-5-21-1234567890-1234567890-1234567890-1001' + + # Assert: Ensure Get-CimInstance was called with 'localhost' + Assert-MockCalled -CommandName Get-CimInstance -ParameterFilter { $ComputerName -eq $env:COMPUTERNAME } -Scope It + } + } + } + + Context 'When the SID is missing for a user' { + It 'should return null and display a warning' { + + InModuleScope -ScriptBlock { + + + # Mock Get-CimInstance to return an object without SID + Mock -CommandName Get-CimInstance -MockWith { + @{ + SID = $null + } + } + + # Mock Write-Warning to capture the warning message + Mock -CommandName Write-Warning + + $computerName = 'Server01' + + # Act: Call the function + $result = Get-SIDFromUsername -Username 'JohnDoe' -ComputerName $computerName + + # Assert: The result should be null + $result | Should -BeNullOrEmpty + + # Assert: Verify that the warning message was displayed + Assert-MockCalled -CommandName Write-Warning -Exactly 1 -Scope It + + } + } + } +} diff --git a/tests/Unit/Private/Helpers/New-DirectoryIfNeeded.tests.ps1 b/tests/Unit/Private/Helpers/New-DirectoryIfNeeded.tests.ps1 new file mode 100644 index 0000000..a27ed8c --- /dev/null +++ b/tests/Unit/Private/Helpers/New-DirectoryIfNeeded.tests.ps1 @@ -0,0 +1,118 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + + +Describe 'New-DirectoryIfNeeded' -Tags "Private", "Helpers" { + # Context: When the directory does not exist + Context 'When the directory does not exist' { + It 'should create the directory and return the created directory object' { + InModuleScope -ScriptBlock { + # Mock Test-Path to return $false (directory does not exist) + Mock -CommandName Test-Path -MockWith { $false } + + # Mock New-Item to simulate directory creation and return a directory object + Mock -CommandName New-Item -MockWith { + @{ + PSIsContainer = $true + Name = 'NewDirectory' + FullName = 'C:\Temp\NewDirectory' + } + } + + # Act: Call the function + $result = New-DirectoryIfNeeded -Directory 'C:\Temp\NewDirectory' + + # Assert: Verify New-Item was called and returned a directory object + $result.PSIsContainer | Should -Be $true + $result.FullName | Should -Be 'C:\Temp\NewDirectory' + + # Assert: Ensure Test-Path and New-Item were called + Assert-MockCalled -CommandName Test-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName New-Item -Exactly 1 -Scope It + } + } + + } + + # Context: When the directory already exists + Context 'When the directory already exists' { + + It 'should return true and not create the directory' { + InModuleScope -ScriptBlock { + # Mock Test-Path to return $true (directory exists) + Mock -CommandName Test-Path -MockWith { $true } + + # Mock New-Item to simulate directory creation + Mock -CommandName New-Item + + + # Act: Call the function + $result = New-DirectoryIfNeeded -Directory 'C:\Temp\ExistingDirectory' + + # Assert: The function should return $true + $result | Should -Be $true + + # Assert: Ensure New-Item was not called since the directory already exists + Assert-MockCalled -CommandName New-Item -Exactly 0 -Scope It + } + } + + } + + # Context: When an error occurs while creating the directory + Context 'When an error occurs during directory creation' { + + It 'should return false and display an error' { + + InModuleScope -ScriptBlock { + + # Mock Test-Path to return $false (directory does not exist) + Mock -CommandName Test-Path -MockWith { $false } + + # Mock New-Item to simulate an error during directory creation + Mock -CommandName New-Item -MockWith { throw "Unable to create directory" } + + # Mock Write-Error to capture the error message + Mock -CommandName Write-Error + + # Act: Call the function + $result = New-DirectoryIfNeeded -Directory 'C:\Temp\NewDirectory' + + # Assert: The function should return $false + $result | Should -Be $false + + # Assert: Verify Write-Error was called + Assert-MockCalled -CommandName Write-Error -Exactly 1 -Scope It + } + } + } + + + # Context: When a mandatory parameter is missing + Context 'When the directory parameter is missing' { + + It 'should throw a missing parameter error' { + InModuleScope -ScriptBlock { + # Act & Assert: Expect the function to throw a missing parameter error + { New-DirectoryIfNeeded -Directory $null } | Should -Throw + { New-DirectoryIfNeeded -Directory "" } | Should -Throw + } + } + } +} diff --git a/tests/Unit/Private/Helpers/ShouldContinueWrapper.tests.ps1 b/tests/Unit/Private/Helpers/ShouldContinueWrapper.tests.ps1 new file mode 100644 index 0000000..778285b --- /dev/null +++ b/tests/Unit/Private/Helpers/ShouldContinueWrapper.tests.ps1 @@ -0,0 +1,101 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +# Unit Tests for ShouldContinueWrapper +Describe 'ShouldContinueWrapper' { + + # Context: When the user chooses to continue + Context 'When the user chooses to continue' { + It 'should return true' { + InModuleScope -ScriptBlock { + # Create a mock context and add the ShouldContinue method + $mockContext = New-Object -TypeName PSObject + $mockContext | Add-Member -MemberType ScriptMethod -Name ShouldContinue -Value { + param($queryMessage, $captionMessage) + return $true + } + + # Act: Call the function + $result = ShouldContinueWrapper -Context $mockContext -QueryMessage "Are you sure?" -CaptionMessage "Confirm Action" + + # Assert: Should return $true + $result | Should -Be $true + } + } + } + + # Context: When the user chooses NOT to continue + Context 'When the user chooses not to continue' { + It 'should return false' { + InModuleScope -ScriptBlock { + # Create a mock context and add the ShouldContinue method + $mockContext = New-Object -TypeName PSObject + $mockContext | Add-Member -MemberType ScriptMethod -Name ShouldContinue -Value { + param($queryMessage, $captionMessage) + return $false + } + + # Act: Call the function + $result = ShouldContinueWrapper -Context $mockContext -QueryMessage "Are you sure?" -CaptionMessage "Confirm Action" + + # Assert: Should return $false + $result | Should -Be $false + } + } + } + + # Context: Ensure parameters are passed correctly + Context 'When parameters are passed correctly' { + It 'should pass the QueryMessage and CaptionMessage to ShouldContinue' { + InModuleScope -ScriptBlock { + # Variables to capture the passed parameters + $env:capturedQueryMessage = $null + $env:capturedCaptionMessage = $null + + # Create a mock context and add the ShouldContinue method + $mockContext = New-Object -TypeName PSObject + + # Create a mock context and add the ShouldContinue method + $mockContext = New-Object -TypeName PSObject + $mockContext | Add-Member -MemberType ScriptMethod -Name ShouldContinue -Value { + param($queryMessage, $captionMessage) + $env:capturedQueryMessage = $queryMessage + $env:capturedCaptionMessage = $captionMessage + return $true + } + + # Act: Call the function + $result = ShouldContinueWrapper -Context $mockContext -QueryMessage "Are you sure?" -CaptionMessage "Confirm Deletion" + + # Assert: Verify that the parameters were passed correctly + $result | Should -Be $true + + # Assert: Verify that the parameters were passed correctly + $env:capturedQueryMessage | Should -Be "Are you sure?" + $env:capturedCaptionMessage | Should -Be "Confirm Deletion" + + # Cleanup: Remove the captured variables + Remove-Item $env:capturedCaptionMessage -Force -ErrorAction SilentlyContinue + Remove-Item $env:capturedQueryMessage -Force -ErrorAction SilentlyContinue + + + } + } + } +} diff --git a/tests/Unit/Private/Helpers/ShouldProcessWrapper.tests.ps1 b/tests/Unit/Private/Helpers/ShouldProcessWrapper.tests.ps1 new file mode 100644 index 0000000..cbefa8f --- /dev/null +++ b/tests/Unit/Private/Helpers/ShouldProcessWrapper.tests.ps1 @@ -0,0 +1,95 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +# Unit Tests for ShouldProcessWrapper +Describe 'ShouldProcessWrapper' { + + # Context: When the user chooses to proceed + Context 'When the user chooses to proceed' { + It 'should return true when user confirms action' { + InModuleScope -ScriptBlock { + # Create a mock context and add the ShouldProcess method + $mockContext = New-Object -TypeName PSObject + $mockContext | Add-Member -MemberType ScriptMethod -Name ShouldProcess -Value { + param($target, $actionMessage) + return $true + } + + # Act: Call the function + $result = ShouldProcessWrapper -Context $mockContext -Target "Server01" -ActionMessage "Delete profiles" + + # Assert: Should return $true + $result | Should -Be $true + } + } + } + + # Context: When the user chooses NOT to proceed + Context 'When the user chooses not to proceed' { + It 'should return false when user declines action' { + InModuleScope -ScriptBlock { + # Create a mock context and add the ShouldProcess method + $mockContext = New-Object -TypeName PSObject + $mockContext | Add-Member -MemberType ScriptMethod -Name ShouldProcess -Value { + param($target, $actionMessage) + return $false + } + + # Act: Call the function + $result = ShouldProcessWrapper -Context $mockContext -Target "Server01" -ActionMessage "Delete profiles" + + # Assert: Should return $false + $result | Should -Be $false + } + } + } + + # Context: Ensure parameters are passed correctly + Context 'When parameters are passed correctly' { + It 'should pass the Target and ActionMessage to ShouldProcess' { + InModuleScope -ScriptBlock { + # Setup environment variables to capture the passed parameters + $env:capturedTarget = $null + $env:capturedActionMessage = $null + + # Create a mock context and add the ShouldProcess method + $mockContext = New-Object -TypeName PSObject + $mockContext | Add-Member -MemberType ScriptMethod -Name ShouldProcess -Value { + param($target, $actionMessage) + $env:capturedTarget = $target + $env:capturedActionMessage = $actionMessage + return $true + } + + # Act: Call the function + $result = ShouldProcessWrapper -Context $mockContext -Target "C:\Temp\File.txt" -ActionMessage "Remove the file" + + # Assert: Should return $true + $result | Should -Be $true + + # Assert: Verify that the parameters were passed correctly + $env:capturedTarget | Should -Be "C:\Temp\File.txt" + $env:capturedActionMessage | Should -Be "Remove the file" + + # Cleanup: Remove the environment variables + Remove-Item Env:capturedTarget, Env:capturedActionMessage -ErrorAction SilentlyContinue + } + } + } +} diff --git a/tests/Unit/Private/Helpers/Test-EnvironmentVariable.tests.ps1 b/tests/Unit/Private/Helpers/Test-EnvironmentVariable.tests.ps1 new file mode 100644 index 0000000..1871a2a --- /dev/null +++ b/tests/Unit/Private/Helpers/Test-EnvironmentVariable.tests.ps1 @@ -0,0 +1,67 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +# Unit Tests for Test-EnvironmentVariable +Describe 'Test-EnvironmentVariable' { + + # Context: When the environment variable exists + Context 'When the environment variable exists' { + It 'should return the value of the environment variable' { + InModuleScope -ScriptBlock { + # Mock the environment variable + $env:TestVariable = "TestValue" + + # Act: Call the function + $result = Test-EnvironmentVariable -Name 'TestVariable' + + # Assert: Should return the value of the environment variable + $result | Should -Be "TestValue" + + # Cleanup: Remove the environment variable + Remove-Item Env:TestVariable -ErrorAction SilentlyContinue + } + } + } + + # Context: When the environment variable does not exist + Context 'When the environment variable does not exist' { + It 'should throw an error indicating the missing environment variable' { + InModuleScope -ScriptBlock { + # Ensure the environment variable does not exist + Remove-Item Env:NonExistentVariable -ErrorAction SilentlyContinue + + # Act & Assert: Call the function and expect an error + { Test-EnvironmentVariable -Name 'NonExistentVariable' } | Should -Throw "Missing required environment variable: NonExistentVariable" + } + } + } + + # Context: Ensure correct behavior when checking common environment variables + Context 'When checking common environment variables' { + It 'should return the value of the Path environment variable if present' { + InModuleScope -ScriptBlock { + # Act: Call the function for 'Path' environment variable + $result = Test-EnvironmentVariable -Name 'Path' + + # Assert: Path environment variable should exist and return its value + $result | Should -Not -BeNullOrEmpty + } + } + } +} diff --git a/tests/Unit/Private/Helpers/Update-JsonFile.tests.ps1 b/tests/Unit/Private/Helpers/Update-JsonFile.tests.ps1 new file mode 100644 index 0000000..b9ca796 --- /dev/null +++ b/tests/Unit/Private/Helpers/Update-JsonFile.tests.ps1 @@ -0,0 +1,181 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +# Unit Tests for Update-JsonFile +Describe 'Update-JsonFile' -tags "Private", "Helpers" { + + + BeforeEach { + + InModuleScope -ScriptBlock { + # Mock filesystem interaction for the tests + Mock -CommandName Test-Path + Mock -CommandName Get-Content + Mock -CommandName Set-Content + Mock -CommandName Out-File + + } + + } + + + # Context: Ensure existing data is correctly handled + Context 'When existing data is not an array' { + It 'should convert the existing data to an array and append the new data' { + InModuleScope -ScriptBlock { + # Mock the behavior of Test-Path to simulate the file exists + Mock Test-Path -MockWith { return $true } + + # Mock the behavior of Get-Content and ConvertFrom-Json to simulate a single non-array object + Mock Get-Content -MockWith { return '{"Name":"HKEY_LOCAL_MACHINE\\Software\\SingleKey","Value":"SingleValue"}' } + + # New registry data to be appended + $newRegistryData = @( + @{ Name = 'HKEY_LOCAL_MACHINE\Software\TestKey'; Value = 'TestValue' } + ) + + # Act: Call the function + Update-JsonFile -OutputFile 'C:\Temp\RegistryData.json' -RegistryData $newRegistryData + + #Asert: Ensure that Set-Content was called to write the updated data back to the file + Assert-MockCalled Set-Content -Exactly 1 -Scope It + } + } + } + + # Test: Create a new file when it doesn't exist + Context "When the JSON file does not exist" { + It "Should create a new file with the provided data" { + + InModuleScope -ScriptBlock { + + Mock Test-Path { return $false } + Mock Out-File + + $registryData = @(@{ Name = 'HKEY_LOCAL_MACHINE\Software\TestKey'; Value = 'TestValue' }) + $outputFile = 'C:\Temp\RegistryData.json' + + Update-JsonFile -OutputFile $outputFile -RegistryData $registryData + + Assert-MockCalled Out-File -Exactly 1 -Scope It + + } + } + } + + # Test: Append data when the file exists + Context "When the JSON file exists" { + + It "Should append the new data to the existing JSON file" { + InModuleScope -ScriptBlock { + Mock Test-Path { return $true } + Mock Get-Content { '[{"Name":"HKEY_LOCAL_MACHINE\\Software\\ExistingKey", "Value":"ExistingValue"}]' } + Mock Set-Content + $existingData = @(@{ Name = 'HKEY_LOCAL_MACHINE\Software\ExistingKey'; Value = 'ExistingValue' }) + $newData = @(@{ Name = 'HKEY_LOCAL_MACHINE\Software\NewKey'; Value = 'NewValue' }) + $outputFile = 'C:\Temp\RegistryData.json' + + Update-JsonFile -OutputFile $outputFile -RegistryData $newData + + Assert-MockCalled Set-Content -Exactly 1 -Scope It + } + } + } + + # Test: Handle invalid or empty data + Context "When invalid data is passed" { + + It "Should throw an error if the RegistryData is not passed" { + InModuleScope -ScriptBlock { + { Update-JsonFile -OutputFile 'C:\Temp\RegistryData.json' -RegistryData $null } | Should -Throw + } + } + } + + # Test: Handle multiple entries in RegistryData + Context "When multiple entries are passed in RegistryData" { + It "Should append all entries to the existing JSON file" { + InModuleScope -ScriptBlock { + Mock Test-Path { return $true } + Mock Get-Content { '[{"Name":"HKEY_LOCAL_MACHINE\\Software\\ExistingKey","Value":"ExistingValue"}]' } + Mock Set-Content + + $registryData = @( + @{ Name = 'HKEY_LOCAL_MACHINE\Software\NewKey1'; Value = 'NewValue1' }, + @{ Name = 'HKEY_LOCAL_MACHINE\Software\NewKey2'; Value = 'NewValue2' } + ) + $outputFile = 'C:\Temp\RegistryData.json' + + Update-JsonFile -OutputFile $outputFile -RegistryData $registryData + + Assert-MockCalled Set-Content -Exactly 1 -Scope It + } + } + } + + + + Context "Ensure correct JSON depth is used" { + It "Should output JSON with a depth of 10" { + InModuleScope -ScriptBlock { + Mock Test-Path { return $false } + Mock ConvertTo-Json {} + Mock Out-File + + $registryData = @(@{ Name = 'HKEY_LOCAL_MACHINE\Software\TestKey'; Value = 'TestValue' }) + $outputFile = 'C:\Temp\RegistryData.json' + + Update-JsonFile -OutputFile $outputFile -RegistryData $registryData + + Assert-MockCalled ConvertTo-Json -Exactly 1 -Scope It -ParameterFilter { + $Depth -eq 10 + } + } + } + } + + # Test: Handle missing OutputFile parameter + Context "When OutputFile is not provided" { + It "Should throw an error" { + InModuleScope -ScriptBlock { + { Update-JsonFile -RegistryData @(@{ Name = 'HKEY_LOCAL_MACHINE\Software\NewKey'; Value = 'NewValue' }) -OutputFile $Null } | Should -Throw + } + } + } + + # Test: Handle empty JSON file + Context "When the JSON file exists but is empty" { + It "Should create a new array with the provided registry data" { + InModuleScope -ScriptBlock { + Mock Test-Path { return $true } + Mock Get-Content { '' } # Simulate empty file content + Mock Set-Content + + $newRegistryData = @(@{ Name = 'HKEY_LOCAL_MACHINE\Software\NewKey'; Value = 'NewValue' }) + $outputFile = 'C:\Temp\RegistryData.json' + + Update-JsonFile -OutputFile $outputFile -RegistryData $newRegistryData + + Assert-MockCalled Set-Content -Exactly 1 -Scope It + } + } + } + + +} diff --git a/tests/Unit/Private/Remove-RegistryKeyForSID.tests.ps1 b/tests/Unit/Private/Remove-RegistryKeyForSID.tests.ps1 deleted file mode 100644 index 823c548..0000000 --- a/tests/Unit/Private/Remove-RegistryKeyForSID.tests.ps1 +++ /dev/null @@ -1,111 +0,0 @@ -BeforeAll { - $script:dscModuleName = "WinProfileOps" - - Import-Module -Name $script:dscModuleName - - $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName - $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName - $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName -} - -AfterAll { - $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') - $PSDefaultParameterValues.Remove('Mock:ModuleName') - $PSDefaultParameterValues.Remove('Should:ModuleName') - - # Unload the module being tested so that it doesn't impact any other tests. - Get-Module -Name $script:dscModuleName -All | Remove-Module -Force -} - -Describe "Remove-RegistryKeyForSID" -Tag 'Private' { - - Context "When the SID registry key is successfully removed" { - - It "Should delete the registry key when confirmed" { - InModuleScope -ScriptBlock { - # Arrange - $sid = "S-1-5-21-123456789-1001" - $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } - $computerName = "Server01" - - Mock Remove-RegistrySubKey { - return $true - } - - # Act - $result = Remove-RegistryKeyForSID -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -Confirm:$false - - # Assert - $result | Should -Be $true - Assert-MockCalled Remove-RegistrySubKey -Exactly 1 -Scope It -ParameterFilter { - $ParentKey -eq $profileListKey -and $SubKeyName -eq $sid -and $ComputerName -eq $computerName - } - } - } - } - - Context "When using -WhatIf parameter" { - - It "Should simulate deletion and not call Remove-RegistrySubKey" { - InModuleScope -ScriptBlock { - # Arrange - $sid = "S-1-5-21-123456789-1001" - $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } - $computerName = "Server01" - - Mock Remove-RegistrySubKey - - # Act - Remove-RegistryKeyForSID -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -WhatIf - - # Assert - Assert-MockCalled Remove-RegistrySubKey -Exactly 0 -Scope It - } - } - } - - Context "When an error occurs while deleting the registry key" { - - It "Should return $false and log an error" { - InModuleScope -ScriptBlock { - # Arrange - $sid = "S-1-5-21-123456789-1001" - $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } - $computerName = "Server01" - - Mock Remove-RegistrySubKey { throw "Registry access error" } - - Mock Write-Error - - # Act - $result = Remove-RegistryKeyForSID -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -Confirm:$false - - # Assert - $result | Should -Be $false - Assert-MockCalled Remove-RegistrySubKey -Exactly 1 -Scope It - Assert-MockCalled Write-Error -Exactly 1 -Scope It - } - } - } - - Context "When SID does not exist in the registry" { - - It "Should return $false if the registry key is not found" { - InModuleScope -ScriptBlock { - # Arrange - $sid = "S-1-5-21-123456789-1001" - $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } - $computerName = "Server01" - - Mock Remove-RegistrySubKey { return $false } - - # Act - $result = Remove-RegistryKeyForSID -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -Confirm:$false - - # Assert - $result | Should -Be $false - Assert-MockCalled Remove-RegistrySubKey -Exactly 1 -Scope It - } - } - } -} diff --git a/tests/Unit/Private/Remove-SIDProfile.tests.ps1 b/tests/Unit/Private/Remove-SIDProfile.tests.ps1 deleted file mode 100644 index 590f5e4..0000000 --- a/tests/Unit/Private/Remove-SIDProfile.tests.ps1 +++ /dev/null @@ -1,146 +0,0 @@ -BeforeAll { - $script:dscModuleName = "WinProfileOps" - - Import-Module -Name $script:dscModuleName - - $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName - $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName - $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName -} - -AfterAll { - $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') - $PSDefaultParameterValues.Remove('Mock:ModuleName') - $PSDefaultParameterValues.Remove('Should:ModuleName') - - # Unload the module being tested so that it doesn't impact any other tests. - Get-Module -Name $script:dscModuleName -All | Remove-Module -Force -} - -Describe "Remove-SIDProfile" -Tag 'Private' { - - Context "When the SID profile registry key is successfully removed" { - - It "Should return a successful ProfileDeletionResult when the key is deleted" { - InModuleScope -ScriptBlock { - # Arrange - $sid = "S-1-5-21-123456789-1001" - $profilePath = "C:\Users\John" - $computerName = "Server01" - $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } - - Mock Remove-RegistryKeyForSID { return $true } - - # Act - $result = Remove-SIDProfile -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -ProfilePath $profilePath -Confirm:$false - - # Assert - $result.GetType().Name | Should -Be 'ProfileDeletionResult' - $result.SID | Should -Be $sid - $result.ProfilePath | Should -Be $profilePath - $result.DeletionSuccess | Should -Be $true - $result.DeletionMessage | Should -Be "Profile registry key for SID '$sid' successfully deleted." - $result.ComputerName | Should -Be $computerName - } - } - - It "Should return a failed ProfileDeletionResult when the key is not deleted" { - InModuleScope -ScriptBlock { - # Arrange - $sid = "S-1-5-21-123456789-1001" - $profilePath = "C:\Users\John" - $computerName = "Server01" - $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } - - Mock Remove-RegistryKeyForSID { return $false } - - # Act - $result = Remove-SIDProfile -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -ProfilePath $profilePath -confirm:$false - - # Assert - $result.GetType().Name | Should -Be 'ProfileDeletionResult' - $result.SID | Should -Be $sid - $result.ProfilePath | Should -Be $profilePath - $result.DeletionSuccess | Should -Be $false - $result.DeletionMessage | Should -Be "Failed to delete the profile registry key for SID '$sid'." - $result.ComputerName | Should -Be $computerName - } - } - } - - Context "When using -WhatIf" { - - It "Should simulate deletion and not call Remove-RegistryKeyForSID" { - InModuleScope -ScriptBlock { - # Arrange - $sid = "S-1-5-21-123456789-1001" - $profilePath = "C:\Users\John" - $computerName = "Server01" - $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } - - Mock Remove-RegistryKeyForSID - - # Act - $result = Remove-SIDProfile -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -ProfilePath $profilePath -WhatIf - - # Assert - Assert-MockCalled Remove-RegistryKeyForSID -Exactly 0 -Scope It - $result.GetType().Name | Should -Be 'ProfileDeletionResult' - $result.SID | Should -Be $sid - $result.DeletionSuccess | Should -Be $false - $result.DeletionMessage | Should -Be "Action skipped." - } - } - } - - Context "When ShouldProcess returns false" { - - It "Should skip deletion and return a skipped ProfileDeletionResult" { - InModuleScope -ScriptBlock { - # Arrange - $sid = "S-1-5-21-123456789-1001" - $profilePath = "C:\Users\John" - $computerName = "Server01" - $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } - - Mock Remove-RegistryKeyForSID - - # Act - $result = Remove-SIDProfile -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -ProfilePath $profilePath -WhatIf - - # Assert - Assert-MockCalled Remove-RegistryKeyForSID -Exactly 0 -Scope It - $result.GetType().Name | Should -Be 'ProfileDeletionResult' - $result.SID | Should -Be $sid - $result.DeletionSuccess | Should -Be $false - $result.DeletionMessage | Should -Be "Action skipped." - } - } - } - - Context "When an error occurs during deletion" { - - It "Should return a failed ProfileDeletionResult and log an error" { - InModuleScope -ScriptBlock { - # Arrange - $sid = "S-1-5-21-123456789-1001" - $profilePath = "C:\Users\John" - $computerName = "Server01" - $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } - - Mock Remove-RegistryKeyForSID { throw "Registry access error" } - #Mock Write-Error - - # Act - $result = Remove-SIDProfile -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -ProfilePath $profilePath -confirm:$false -ErrorAction Continue - - # Assert - $result.GetType().Name | Should -Be 'ProfileDeletionResult' - $result.SID | Should -Be $sid - $result.DeletionSuccess | Should -Be $false - $result.DeletionMessage | Should -Be "Failed to delete the profile registry key for SID '$sid'. Error: Registry access error" - #Assert-MockCalled Write-Error -Exactly 1 -Scope It - } - } - } -} diff --git a/tests/Unit/Private/RemoveProfileReg/Backup-RegistryKeyForSID.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/Backup-RegistryKeyForSID.tests.ps1 new file mode 100644 index 0000000..a2bdf1e --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/Backup-RegistryKeyForSID.tests.ps1 @@ -0,0 +1,247 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Backup-RegistryKeyForSID' -Tag 'Private', "RemoveProfileReg" { + + + BeforeEach { + InModuleScope -ScriptBlock { + # Mock dependencies + Mock -CommandName New-DirectoryIfNeeded + Mock -CommandName New-RegistryKeyValuesObject + Mock -CommandName Update-JsonFile + Mock -CommandName Write-Error + } + } + + # Test: Backup success case + Context 'When the backup is successful' { + It 'Should create a backup of the registry key and return $true' { + InModuleScope -ScriptBlock { + # Mock the directory creation to succeed + Mock New-DirectoryIfNeeded { return $true } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock registry key backup to return a valid object + Mock New-RegistryKeyValuesObject { + return @{ BackUpDate = (Get-Date) } + } + + # Call the function with mock data + $result = Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + + # Assert that the registry key was backed up + Assert-MockCalled Update-JsonFile -Exactly 1 -Scope It + + # Ensure the function returned true + $result | Should -Be $true + } + } + } + + # Test: Backup directory creation failure + Context 'When the backup directory cannot be created' { + It 'Should return $false and write an error' { + InModuleScope -ScriptBlock { + # Mock directory creation to fail + Mock New-DirectoryIfNeeded { return $false } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Call the function with mock data + $result = Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + + # Ensure it wrote an error message + Assert-MockCalled Write-Error -Exactly 1 -Scope It + + # Ensure the function returned false + $result | Should -Be $false + } + } + } + + # Test: Registry key backup failure + Context 'When the registry key backup fails' { + It 'Should return $false' { + InModuleScope -ScriptBlock { + # Mock directory creation to succeed + Mock New-DirectoryIfNeeded { return $true } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock registry key backup to throw an error + Mock New-RegistryKeyValuesObject { throw "Registry key backup failed" } + + # Call the function with mock data + $result = Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + + # Ensure an error was written + Assert-MockCalled Write-Error -Exactly 1 -Scope It + + # Ensure the function returned false + $result | Should -Be $false + } + } + } + + # Test: Exception handling + Context 'When an unexpected error occurs' { + It 'Should catch the exception and return $false' { + InModuleScope -ScriptBlock { + # Mock directory creation to succeed + Mock New-DirectoryIfNeeded { return $true } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock registry key backup to succeed + Mock New-RegistryKeyValuesObject { return @{ BackUpDate = (Get-Date) } } + + # Mock Update-JsonFile to throw an exception + Mock Update-JsonFile { throw "Unexpected error" } + + # Call the function with mock data + $result = Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + + # Ensure an error was written + Assert-MockCalled Write-Error -Exactly 1 -Scope It + + # Ensure the function returned false + $result | Should -Be $false + } + } + } + + Context 'When using a network path for the backup directory' { + It 'Should successfully create a backup on a network path' { + InModuleScope -ScriptBlock { + # Mock directory creation to succeed + Mock New-DirectoryIfNeeded { return $true } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock registry key backup to return a valid object + Mock New-RegistryKeyValuesObject { + return @{ BackUpDate = (Get-Date) } + } + + # Call the function with a network path + $result = Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory '\\Server01\Backups' -ComputerName 'Server01' + + # Assert that the registry key was backed up + Assert-MockCalled Update-JsonFile -Exactly 1 -Scope It + + # Ensure the function returned true + $result | Should -Be $true + } + } + } + + + Context 'When the registry key data is empty or null' { + It 'Should return $false and write an error' { + InModuleScope -ScriptBlock { + # Mock directory creation to succeed + Mock New-DirectoryIfNeeded { return $true } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock registry key backup to return null or empty data + Mock New-RegistryKeyValuesObject { return $null } + + # Call the function with mock data + $result = Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + + # Ensure an error was written + Assert-MockCalled Write-Error -Exactly 1 -Scope It + + # Ensure the function returned false + $result | Should -Be $false + } + } + } + + Context 'When the backup directory exists but is not writable' { + It 'Should return $false and write an error' { + InModuleScope -ScriptBlock { + # Mock directory creation to succeed + Mock New-DirectoryIfNeeded { return $true } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock Update-JsonFile to throw a permission error + Mock Update-JsonFile { throw "Permission denied" } + + # Call the function with mock data + $result = Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + + # Ensure an error was written + Assert-MockCalled Write-Error -Exactly 1 -Scope It + + # Ensure the function returned false + $result | Should -Be $false + } + } + } + + Context 'When an invalid or empty SID is provided' { + It 'Should throw a ParameterBindingValidationException' { + InModuleScope -ScriptBlock { + # Call the function with an empty SID and verify it throws the expected error + { Backup-RegistryKeyForSID -SID '' -BaseKey $null -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' } | Should -Throw + } + } + } + + Context 'When backing up the registry key with New-RegistryKeyValuesObject' { + It 'Should call New-RegistryKeyValuesObject and set BackUpDate in ISO 8601 format' { + InModuleScope -ScriptBlock { + # Mock the directory creation to succeed + Mock New-DirectoryIfNeeded { return $true } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock New-RegistryKeyValuesObject to return a mock object with a BackUpDate property + Mock New-RegistryKeyValuesObject { + return [pscustomobject]@{ BackUpDate = (Get-Date) } + } + + # Call the function + Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + + # Verify that New-RegistryKeyValuesObject was called with the correct parameters + Assert-MockCalled New-RegistryKeyValuesObject -Exactly 1 -Scope It -ParameterFilter { + $RegistryKey -eq $BaseKey -and $ComputerName -eq 'Server01' -and $SubKeyName -eq 'S-1-5-21-12345' + } + + # Verify that the BackUpDate was set correctly in ISO 8601 format + $expectedDate = (Get-Date).ToString("o") + Mock Update-JsonFile -MockWith { + param ($OutputFile, $RegistryData) + $RegistryData.BackUpDate | Should -Be $expectedDate + } + + # Call the function again to test the BackupDate + Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + } + } + } + + +} diff --git a/tests/Unit/Private/RemoveProfileReg/Confirm-ProfileRemoval.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/Confirm-ProfileRemoval.tests.ps1 new file mode 100644 index 0000000..aac8674 --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/Confirm-ProfileRemoval.tests.ps1 @@ -0,0 +1,207 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Confirm-ProfileRemoval' -Tag 'Private', "RemoveProfileReg" { + + BeforeEach { + InModuleScope -ScriptBlock { + # Mock Write-Error + Mock -CommandName Write-Error + } + } + + # Test: Registry key exists (SID is found in subkeys) + Context 'When the SID exists in the registry subkeys' { + It 'Should return $false' { + InModuleScope -ScriptBlock { + # Create a mock RegistryKey object with GetSubKeyNames() returning an array that contains the SID + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { return @('S-1-5-21-12345', 'S-1-5-21-67890') } + } + + # Call the function + $result = Confirm-ProfileRemoval -SID 'S-1-5-21-12345' -BaseKey $BaseKey + + # Ensure the function returns $false because the SID exists + $result | Should -Be $false + } + } + } + + # Test: Registry key does not exist (SID is not found in subkeys) + Context 'When the SID does not exist in the registry subkeys' { + It 'Should return $true' { + InModuleScope -ScriptBlock { + # Create a mock RegistryKey object with GetSubKeyNames() returning an array that does not contain the SID + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { return @('S-1-5-21-67890', 'S-1-5-21-99999') } + } + + # Call the function + $result = Confirm-ProfileRemoval -SID 'S-1-5-21-12345' -BaseKey $BaseKey + + # Ensure the function returns $true because the SID does not exist + $result | Should -Be $true + } + } + } + + # Test: Exception occurs while accessing registry + Context 'When there is an error accessing the registry' { + It 'Should return $false and write an error' { + InModuleScope -ScriptBlock { + # Create a mock RegistryKey object that throws an error when GetSubKeyNames() is called + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { throw "Registry access error" } + } + + # Call the function + $result = Confirm-ProfileRemoval -SID 'S-1-5-21-12345' -BaseKey $BaseKey + + # Ensure an error was written + Assert-MockCalled Write-Error -Exactly 1 -Scope It + + # Ensure the function returns $false due to the exception + $result | Should -Be $false + } + } + } + + # Test: When BaseKey is empty or null + Context 'When BaseKey is null' { + It 'Should return an False and write an error' { + InModuleScope -ScriptBlock { + + Mock Write-Error {} + + # Call the function with a null BaseKey + $Return = Confirm-ProfileRemoval -SID 'S-1-5-21-12345' -BaseKey $null + + #Assert Return is false + $Return | Should -Be $false + + # Ensure an error was written + Assert-MockCalled Write-Error -Exactly 1 -Scope It + } + } + } + + # Test: Empty array from GetSubKeyNames (No subkeys) + Context 'When the registry key has no subkeys' { + It 'Should return $true because there are no subkeys' { + InModuleScope -ScriptBlock { + # Create a mock RegistryKey object with GetSubKeyNames() returning an empty array + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { return @() } + } + + # Call the function + $result = Confirm-ProfileRemoval -SID 'S-1-5-21-12345' -BaseKey $BaseKey + + # Ensure the function returns $true because there are no subkeys + $result | Should -Be $true + } + } + } + + # Test: Multiple subkeys but none matching the SID + Context 'When there are multiple subkeys but none match the SID' { + It 'Should return $true' { + InModuleScope -ScriptBlock { + # Create a mock RegistryKey object with GetSubKeyNames() returning several subkeys, but none match the SID + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { return @('S-1-5-21-67890', 'S-1-5-21-99999', 'S-1-5-21-11111') } + } + + # Call the function + $result = Confirm-ProfileRemoval -SID 'S-1-5-21-12345' -BaseKey $BaseKey + + # Ensure the function returns $true because the SID does not exist among the subkeys + $result | Should -Be $true + } + } + } + + # Test: When the SID is a non-standard or invalid format + Context 'When an invalid or malformed SID is provided' { + It 'Should return $true' { + InModuleScope -ScriptBlock { + # Create a mock RegistryKey object with GetSubKeyNames() returning a list of subkeys + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { return @('S-1-5-21-67890') } + } + + # Call the function with an invalid SID + $result = Confirm-ProfileRemoval -SID 'INVALID-SID' -BaseKey $BaseKey + + # Ensure the function returns $false since it's an invalid SID + $result | Should -Be $true + } + } + } + + # Test: Case sensitivity for SID matching + Context 'When the SID is provided in a different case' { + It 'Should still return the correct result regardless of case sensitivity' { + InModuleScope -ScriptBlock { + # Create a mock RegistryKey object with GetSubKeyNames() returning a list of subkeys + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { return @('S-1-5-21-12345') } + } + + # Call the function with a lower-case SID + $result = Confirm-ProfileRemoval -SID 's-1-5-21-12345' -BaseKey $BaseKey + + # Ensure the function returns $false because the SID exists, regardless of case + $result | Should -Be $false + } + } + } + + # Test: Handle large number of subkeys + Context 'When there is a very large number of subkeys' { + It 'Should return $true or $false depending on SID existence' { + InModuleScope -ScriptBlock { + # Generate a large list of SIDs + $subKeys = 1..100 | ForEach-Object { "S-1-5-21-$_" } + + # Create a mock RegistryKey object with GetSubKeyNames() returning a large array of subkeys + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { return $subKeys } + } + + # Call the function with a SID that does not exist in the large array + $result = Confirm-ProfileRemoval -SID 'S-1-5-21-110' -BaseKey $BaseKey + + # Ensure the function returns $true because the SID does not exist + $result | Should -Be $true + + # Call the function with a SID that exists in the large array + $result = Confirm-ProfileRemoval -SID 'S-1-5-21-99' -BaseKey $BaseKey + + # Ensure the function returns $false because the SID exists + $result | Should -Be $false + } + } + } + + + + +} diff --git a/tests/Unit/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.tests.ps1 new file mode 100644 index 0000000..8e26ffe --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.tests.ps1 @@ -0,0 +1,257 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Invoke-UserProfileRegRemoval' -Tags 'Private', 'UserProfileReg' { + + BeforeEach { + InModuleScope -ScriptBlock { + # Mocking necessary dependencies + Mock -CommandName Invoke-UserProfileAudit + Mock -CommandName Remove-ProfileRegistryKey + Mock -CommandName Write-Error + Mock ShouldContinueWrapper { + param($Context, $QueryMessage, $CaptionMessage) + return $true + } + } + } + + Context 'When confirmation is required' { + It 'Should call ShouldContinueWrapper before removing profile' { + InModuleScope -ScriptBlock { + + Mock -CommandName Remove-UserProfileRegistryEntry {} + + # Mock the registry key opening to succeed + Mock Open-RegistryKey { return New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ Close = {} } } + + # Mock profile audit results using New-UserProfileObject + Mock Invoke-UserProfileAudit { + $mockAuditResults = [UserProfile[]]@() + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12346' -ProfilePath 'C:\Users\Test2' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + return $mockAuditResults + } + + # Call the function with the Confirm switch + Invoke-UserProfileRegRemoval -ComputerName 'Server01' -SID 'S-1-5-21-12345' -RegistryPath 'Path' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' + + # Ensure ShouldContinueWrapper was called + Assert-MockCalled ShouldContinueWrapper -Exactly 1 -Scope It -ParameterFilter { + $QueryMessage -like '*S-1-5-21-12345*' + } + + # Ensure that Remove-UserProfileRegistryEntry was called after confirmation + Assert-MockCalled Remove-UserProfileRegistryEntry -Exactly 1 -Scope It + } + } + } + + # Test: When in audit mode (AuditOnly switch is used) + Context 'When in audit mode' { + It 'Should perform an audit and not remove the profiles' { + InModuleScope -ScriptBlock { + # Mock the registry key opening to succeed + Mock Open-RegistryKey { return New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ Close = {} } } + + Mock Remove-UserProfileRegistryEntry { + param($SelectedProfile, $BaseKey, $AuditOnly) + $deletionResultParams = @{ + SID = $SelectedProfile.SID + ProfilePath = $SelectedProfile.ProfilePath + ComputerName = $SelectedProfile.ComputerName + DeletionSuccess = $false + DeletionMessage = "Profile not removed." + } + + # If in audit mode, output an audit-only result directly to the pipeline and return + if ($AuditOnly) + { + $deletionResultParams.DeletionSuccess = $true + $deletionResultParams.DeletionMessage = "Audit only, no deletion performed." + New-ProfileDeletionResult @deletionResultParams + return # Return to allow pipeline to continue with the next item + } + + } + + # Mock profile audit results using New-UserProfileObject + Mock Invoke-UserProfileAudit { + $mockAuditResults = [UserProfile[]]@() + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12346' -ProfilePath 'C:\Users\Test2' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + return $mockAuditResults + } + + # Call the function with AuditOnly switch + $result = Invoke-UserProfileRegRemoval -ComputerName 'Server01' -SID 'S-1-5-21-12345' -RegistryPath 'Path' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' -AuditOnly + + # Ensure that Remove-UserProfileRegistryEntry was NOT called + Assert-MockCalled Remove-UserProfileRegistryEntry -Times 1 -Scope It -ParameterFilter { + $AuditOnly -eq $true + } + } + } + } + + # Test: When using Force switch + Context 'When using Force switch' { + It 'Should remove the profile without confirmation' { + InModuleScope -ScriptBlock { + + Mock -CommandName Remove-UserProfileRegistryEntry {} + + + # Mock the registry key opening to succeed + Mock Open-RegistryKey { return New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ Close = {} } } + + # Mock profile audit results using New-UserProfileObject + Mock Invoke-UserProfileAudit { + $mockAuditResults = [UserProfile[]]@() + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + return $mockAuditResults + } + + # Call the function with the Force switch + Invoke-UserProfileRegRemoval -ComputerName 'Server01' -SID 'S-1-5-21-12345' -RegistryPath 'Path' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' -Force + + # Ensure that ShouldContinueWrapper was not called (no confirmation) + Assert-MockCalled ShouldContinueWrapper -Exactly 0 -Scope It + + # Ensure Remove-UserProfileRegistryEntry was called + Assert-MockCalled Remove-UserProfileRegistryEntry -Exactly 1 -Scope It + } + } + } + + # Test: When registry key cannot be opened + Context 'When registry key cannot be opened' { + It 'Should write an error and exit' { + InModuleScope -ScriptBlock { + + Mock -CommandName Remove-UserProfileRegistryEntry {} + # Mock the registry key opening to fail + Mock Open-RegistryKey { + param($ComputerName, $RegistryHive, $RegistryPath) + $Out = $Null + return $Out + } + + # Call the function + Invoke-UserProfileRegRemoval -ComputerName 'Server01' -SID 'S-1-5-21-12345' -RegistryPath 'Path' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' + + # Ensure Write-Error was called + Assert-MockCalled Write-Error -Exactly 1 -Scope It + + # Ensure Remove-UserProfileRegistryEntry was not called + Assert-MockCalled Remove-UserProfileRegistryEntry -Exactly 0 -Scope It + } + } + } + + # Test: When processing multiple SIDs from the pipeline + Context 'When processing multiple SIDs' { + It 'Should handle multiple SIDs from the pipeline' { + InModuleScope -ScriptBlock { + + Mock -CommandName Remove-UserProfileRegistryEntry {} + + # Mock the registry key opening to succeed + Mock Open-RegistryKey { return New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ Close = {} } } + + # Mock profile audit results using New-UserProfileObject + Mock Invoke-UserProfileAudit { + $mockAuditResults = [UserProfile[]]@() + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12346' -ProfilePath 'C:\Users\Test2' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + return $mockAuditResults + } + + # Call the function with multiple SIDs from the pipeline + 'S-1-5-21-12345', 'S-1-5-21-12346' | Invoke-UserProfileRegRemoval -ComputerName 'Server01' -RegistryPath 'Path' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' -Force + + # Ensure that Remove-UserProfileRegistryEntry was called for each SID + Assert-MockCalled Remove-UserProfileRegistryEntry -Exactly 2 -Scope It + } + } + } + + # Test: When profile removal fails + Context 'When profile removal fails' { + It 'Should return deletion object with failure message' { + InModuleScope -ScriptBlock { + # Mock the registry key opening to succeed + Mock Open-RegistryKey { return New-MockObject -Type "Microsoft.Win32.RegistryKey" } + + Mock Backup-RegistryKeyForSID { return $true } + + Mock Remove-ProfileRegistryKey { return $false } + + # Mock profile audit results using New-UserProfileObject + Mock Invoke-UserProfileAudit { + $mockAuditResults = @() + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + return $mockAuditResults + } + + # Call the function + $result = Invoke-UserProfileRegRemoval -ComputerName 'Server01' -SID 'S-1-5-21-12345' -RegistryPath 'Path' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' + + # Ensure an error was written + $result.DeletionMessage | Should -Be "Failed to remove profile registry key." + + } + } + } + + Context 'When a special profile is encountered' { + It 'Should handle the special profile appropriately' { + InModuleScope -ScriptBlock { + + Mock Remove-UserProfileRegistryEntry {} + + # Mock the registry key opening to succeed + Mock Open-RegistryKey { return New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ Close = {} } } + + # Mock profile audit results with a special profile + Mock Invoke-UserProfileAudit { + param($ignoreSpecial) + + if ($ignoreSpecial) + { + return @() + } + else + { + return @( + New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $true + ) + } + } + + # Call the function with the special profile + Invoke-UserProfileRegRemoval -ComputerName 'Server01' -SID 'S-1-5-21-12345' -RegistryPath 'Path' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' + + # Ensure Remove-UserProfileRegistryEntry was not called for special profile + Assert-MockCalled Remove-UserProfileRegistryEntry -Exactly 0 -Scope It + } + } + } + +} diff --git a/tests/Unit/Private/RemoveProfileReg/New-ProfileDeletionResult.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/New-ProfileDeletionResult.tests.ps1 new file mode 100644 index 0000000..f08f7b9 --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/New-ProfileDeletionResult.tests.ps1 @@ -0,0 +1,87 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'New-ProfileDeletionResult' -Tags 'Private', 'UserProfile' { + + BeforeEach { + # Setup any common prerequisites or mocks + InModuleScope -ScriptBlock { + # Clear any mocks or global variables if needed + } + } + + # Test: Full parameter set + Context 'When creating a full ProfileDeletionResult object' { + It 'Should create a ProfileDeletionResult object with all properties' { + InModuleScope -ScriptBlock { + # Call the function with all parameters + $result = New-ProfileDeletionResult -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -DeletionSuccess $true -DeletionMessage 'Profile removed successfully.' -ComputerName 'Server01' + + # Validate the result object + $result.SID | Should -Be 'S-1-5-21-12345' + $result.ProfilePath | Should -Be 'C:\Users\Test1' + $result.DeletionSuccess | Should -Be $true + $result.DeletionMessage | Should -Be 'Profile removed successfully.' + $result.ComputerName | Should -Be 'Server01' + } + } + } + + # Test: SuccessOnly parameter set + Context 'When creating a ProfileDeletionResult with SuccessOnly parameter set' { + It 'Should create a ProfileDeletionResult object with only SID and DeletionSuccess' { + InModuleScope -ScriptBlock { + # Call the function with SID and DeletionSuccess only + $result = New-ProfileDeletionResult -SID 'S-1-5-21-67890' -DeletionSuccess $false + + # Validate the result object + $result.SID | Should -Be 'S-1-5-21-67890' + $result.DeletionSuccess | Should -Be $false + $result.ProfilePath | Should -BeNullOrEmpty + $result.DeletionMessage | Should -Be 'Operation failed' + $result.ComputerName | Should -Be $env:COMPUTERNAME + } + } + } + + # Test: Minimal parameter set + Context 'When creating a minimal ProfileDeletionResult object' { + It 'Should create a ProfileDeletionResult object with only SID' { + InModuleScope -ScriptBlock { + # Call the function with only SID + $result = New-ProfileDeletionResult -SID 'S-1-5-21-99999' + + # Validate the result object + $result.SID | Should -Be 'S-1-5-21-99999' + $result.DeletionSuccess | Should -Be $false + $result.ProfilePath | Should -BeNullOrEmpty + $result.DeletionMessage | Should -Be 'No action performed' + $result.ComputerName | Should -Be $env:COMPUTERNAME + } + } + } + # Test: Edge case with no SID provided + Context 'When creating a ProfileDeletionResult without SID' { + It 'Should throw an error because SID is mandatory' { + InModuleScope -ScriptBlock { + { New-ProfileDeletionResult -DeletionSuccess $true } | Should -Throw -ErrorId 'AmbiguousParameterSet,New-ProfileDeletionResult' + } + } + } +} diff --git a/tests/Unit/Private/RemoveProfileReg/PromptForConfirmation.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/PromptForConfirmation.tests.ps1 new file mode 100644 index 0000000..3d0ed63 --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/PromptForConfirmation.tests.ps1 @@ -0,0 +1,129 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'PromptForConfirmation' -Tags 'Private', 'Helpers' { + + BeforeEach { + InModuleScope -ScriptBlock { + # Mock the ShouldContinueWrapper for testing user confirmation + Mock ShouldContinueWrapper { + param($Context, $QueryMessage, $CaptionMessage) + return $true # Simulate that the user confirms + } + } + } + + # Test: AuditOnly switch skips the confirmation + Context 'When AuditOnly is used' { + It 'Should skip confirmation and return $true' { + InModuleScope -ScriptBlock { + # Call the function with AuditOnly switch + $result = PromptForConfirmation -ComputerName 'Server01' -ItemCount 5 -AuditOnly + + # Validate that the result is true and confirmation is skipped + $result | Should -Be $true + + # Ensure ShouldContinueWrapper was not called + Assert-MockCalled ShouldContinueWrapper -Exactly 0 -Scope It + } + } + } + + # Test: Confirm switch prompts for confirmation + Context 'When Confirm is specified' { + It 'Should prompt the user for confirmation' { + InModuleScope -ScriptBlock { + + $mockContext = New-Object -TypeName PSObject + + # Call the function with Confirm switch + $result = PromptForConfirmation -ComputerName 'Server01' -ItemCount 5 -Confirm -context $mockContext + + # Validate that the result is true, assuming the user confirms + $result | Should -Be $true + + # Ensure ShouldContinueWrapper was called with the correct parameters + Assert-MockCalled ShouldContinueWrapper -Exactly 1 -Scope It -ParameterFilter { + $QueryMessage -eq "Are you sure you want to delete 5 profiles from Server01's registry?" -and + $CaptionMessage -eq "Confirm Deletion" + } + } + } + } + + # Test: User declines the confirmation + Context 'When user declines the confirmation' { + It 'Should return $false when the user declines the prompt' { + InModuleScope -ScriptBlock { + # Mock ShouldContinueWrapper to simulate user declining + Mock ShouldContinueWrapper { + return $false # Simulate that the user declines + } + + $mockContext = New-Object -TypeName PSObject + + # Call the function with Confirm switch + $result = PromptForConfirmation -ComputerName 'Server01' -ItemCount 5 -Confirm -context $mockContext + # Validate that the result is false, since the user declined + $result | Should -Be $false + + # Ensure ShouldContinueWrapper was called + Assert-MockCalled ShouldContinueWrapper -Exactly 1 -Scope It + } + } + } + + # Test: No Confirm or AuditOnly specified, should proceed without prompt + Context 'When neither AuditOnly nor Confirm are specified' { + It 'Should proceed without prompting and return $true' { + InModuleScope -ScriptBlock { + + $mockContext = New-Object -TypeName PSObject + + # Call the function without AuditOnly or Confirm + $result = PromptForConfirmation -ComputerName 'Server01' -ItemCount 5 -context $mockContext + + # Validate that the result is true, proceeding without confirmation + $result | Should -Be $true + + # Ensure ShouldContinueWrapper was not called + Assert-MockCalled ShouldContinueWrapper -Exactly 0 -Scope It + } + } + } + + # Test: Confirm is false, should proceed without prompting + Context 'When Confirm is explicitly set to $false' { + It 'Should proceed without prompting and return $true' { + InModuleScope -ScriptBlock { + + $mockContext = New-Object -TypeName PSObject + + # Call the function with Confirm set to false + $result = PromptForConfirmation -ComputerName 'Server01' -ItemCount 5 -Confirm:$false -context $mockContext + + # Validate that the result is true, proceeding without confirmation + $result | Should -Be $true + + # Ensure ShouldContinueWrapper was not called + Assert-MockCalled ShouldContinueWrapper -Exactly 0 -Scope It + } + } + } +} diff --git a/tests/Unit/Private/RemoveProfileReg/Remove-UserProfileRegistryEntry.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/Remove-UserProfileRegistryEntry.tests.ps1 new file mode 100644 index 0000000..166e4e8 --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/Remove-UserProfileRegistryEntry.tests.ps1 @@ -0,0 +1,233 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Remove-UserProfileRegistryEntry' -Tags 'Private', 'UserProfileReg' { + + BeforeEach { + InModuleScope -ScriptBlock { + # Mock functions used in the Remove-UserProfileRegistryEntry function + Mock -CommandName Backup-RegistryKeyForSID + Mock -CommandName Remove-ProfileRegistryKey + Mock -CommandName Confirm-ProfileRemoval + Mock -CommandName New-ProfileDeletionResult + + } + } + + # Test: Audit mode (AuditOnly switch is used) + Context 'When in audit mode' { + It 'Should return a ProfileDeletionResult with audit-only message and success' { + InModuleScope -ScriptBlock { + + # Use New-UserProfileObject to mock UserProfile objects + $mockUserProfile = New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath "C:\Users\TestUser1" -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock the base registry key + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + + # Call the function in AuditOnly mode + $result = Remove-UserProfileRegistryEntry -SelectedProfile $mockUserProfile -BaseKey $BaseKey -AuditOnly + + # Ensure the audit result is returned with success and audit message + Assert-MockCalled New-ProfileDeletionResult -Exactly 1 -Scope It -ParameterFilter { + $DeletionMessage -eq 'Audit only, no deletion performed.' -and + $DeletionSuccess -eq $true + } + } + } + } + + # Test: Backup failure + Context 'When backup of the profile fails' { + It 'Should return a ProfileDeletionResult indicating backup failure' { + InModuleScope -ScriptBlock { + + # Use New-UserProfileObject to mock UserProfile objects + $mockUserProfile = New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath "C:\Users\TestUser1" -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock the base registry key + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock Backup-RegistryKeyForSID to return false (backup failure) + Mock Backup-RegistryKeyForSID { return $false } + + # Call the function + $result = Remove-UserProfileRegistryEntry -SelectedProfile $mockUserProfile -BaseKey $BaseKey + + # Ensure the result indicates backup failure + Assert-MockCalled New-ProfileDeletionResult -Exactly 1 -Scope It -ParameterFilter { + $DeletionMessage -eq 'Failed to backup profile.' -and + $DeletionSuccess -eq $false + } + + # Ensure no attempt was made to remove the profile if the backup failed + Assert-MockCalled Remove-ProfileRegistryKey -Exactly 0 -Scope It + } + } + } + + # Test: Successful profile removal + Context 'When profile removal is successful' { + It 'Should return a ProfileDeletionResult indicating success' { + InModuleScope -ScriptBlock { + + # Use New-UserProfileObject to mock UserProfile objects + $mockUserProfile = New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath "C:\Users\TestUser1" -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock the base registry key + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + + # Mock Backup-RegistryKeyForSID and Remove-ProfileRegistryKey to succeed + Mock Backup-RegistryKeyForSID { return $true } + Mock Remove-ProfileRegistryKey { return $true } + + # Mock Confirm-ProfileRemoval to return true (successful removal) + Mock Confirm-ProfileRemoval { return $true } + + # Call the function + $result = Remove-UserProfileRegistryEntry -SelectedProfile $mockUserProfile -BaseKey $BaseKey + + # Ensure the result indicates successful removal + Assert-MockCalled New-ProfileDeletionResult -Exactly 1 -Scope It -ParameterFilter { + $DeletionMessage -eq 'Profile removed successfully.' -and + $DeletionSuccess -eq $true + } + } + } + } + + # Test: Confirm-ProfileRemoval succeeds after backup and registry removal + Context 'When profile is successfully backed up, removed, and confirmed' { + It 'Should return a ProfileDeletionResult indicating full success' { + InModuleScope -ScriptBlock { + + # Use New-UserProfileObject to mock UserProfile objects + $mockUserProfile = New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath "C:\Users\TestUser1" -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock the base registry key + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock Backup-RegistryKeyForSID and Remove-ProfileRegistryKey to succeed + Mock Backup-RegistryKeyForSID { return $true } + Mock Remove-ProfileRegistryKey { return $true } + + # Mock Confirm-ProfileRemoval to succeed + Mock Confirm-ProfileRemoval { return $true } + + # Call the function + $result = Remove-UserProfileRegistryEntry -SelectedProfile $mockUserProfile -BaseKey $BaseKey + + # Ensure the result indicates full success + Assert-MockCalled New-ProfileDeletionResult -Exactly 1 -Scope It -ParameterFilter { + $DeletionMessage -eq "Profile removed successfully." -and + $DeletionSuccess -eq $true + } + } + } + } + + + # Test: Failed profile removal + Context 'When profile removal fails after a successful backup' { + It 'Should return a ProfileDeletionResult indicating removal failure' { + InModuleScope -ScriptBlock { + + # Use New-UserProfileObject to mock UserProfile objects + $mockUserProfile = New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath "C:\Users\TestUser1" -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock the base registry key + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock Backup-RegistryKeyForSID to succeed + Mock Backup-RegistryKeyForSID { return $true } + + # Mock Remove-ProfileRegistryKey to fail + Mock Remove-ProfileRegistryKey { return $false } + + # Call the function + $result = Remove-UserProfileRegistryEntry -SelectedProfile $mockUserProfile -BaseKey $BaseKey + + # Ensure the result indicates profile removal failure + Assert-MockCalled New-ProfileDeletionResult -Exactly 1 -Scope It -ParameterFilter { + $DeletionMessage -eq 'Failed to remove profile registry key.' -and + $DeletionSuccess -eq $false + } + } + } + } + + # Test: Failed profile removal confirmation + Context 'When profile removal is successful but confirmation fails' { + It 'Should return a ProfileDeletionResult indicating removal failure' { + InModuleScope -ScriptBlock { + + # Use New-UserProfileObject to mock UserProfile objects + $mockUserProfile = New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath "C:\Users\TestUser1" -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock the base registry key + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock Backup-RegistryKeyForSID and Remove-ProfileRegistryKey to succeed + Mock Backup-RegistryKeyForSID { return $true } + Mock Remove-ProfileRegistryKey { return $true } + + # Mock Confirm-ProfileRemoval to fail + Mock Confirm-ProfileRemoval { return $false } + + # Call the function + $result = Remove-UserProfileRegistryEntry -SelectedProfile $mockUserProfile -BaseKey $BaseKey + + # Ensure the result indicates profile removal failure despite registry key removal + Assert-MockCalled New-ProfileDeletionResult -Exactly 1 -Scope It -ParameterFilter { + $DeletionMessage -eq "Profile removal verification failed." -and + $DeletionSuccess -eq $false + } + } + } + } + + + # Test: No BaseKey provided + Context 'When no base registry key is provided' { + It 'Should return a ProfileDeletionResult indicating failure due to missing BaseKey' { + InModuleScope -ScriptBlock { + + # Use New-UserProfileObject to mock UserProfile objects + $mockUserProfile = New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath "C:\Users\TestUser1" -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Call the function with a null BaseKey + $result = Remove-UserProfileRegistryEntry -SelectedProfile $mockUserProfile -BaseKey $null + + # Ensure the result indicates failure due to missing BaseKey + Assert-MockCalled New-ProfileDeletionResult -Exactly 1 -Scope It -ParameterFilter { + $DeletionMessage -eq "Failed: BaseKey is null, cannot remove the profile." -and + $DeletionSuccess -eq $false + } + + # Ensure no backup or profile removal was attempted + Assert-MockCalled Backup-RegistryKeyForSID -Exactly 0 -Scope It + Assert-MockCalled Remove-ProfileRegistryKey -Exactly 0 -Scope It + } + } + } + + +} diff --git a/tests/Unit/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.tests.ps1 new file mode 100644 index 0000000..84e5506 --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.tests.ps1 @@ -0,0 +1,162 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Resolve-UserProfileForDeletion' -Tags 'Private', 'UserProfileReg' { + + BeforeEach { + InModuleScope -ScriptBlock { + # Mock the Validate-SIDFormat and New-ProfileDeletionResult functions + Mock -CommandName Validate-SIDFormat + Mock -CommandName Write-Warning + } + } + + # Test: Successfully finding a profile + Context 'When the profile exists in the audit results' { + It 'Should return the UserProfile object' { + InModuleScope -ScriptBlock { + # Mock audit results with valid UserProfile objects + $mockAuditResults = @() + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-1002' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + + # Call the function with a valid SID + $result = Resolve-UserProfileForDeletion -SID 'S-1-5-21-1001' -AuditResults $mockAuditResults -ComputerName 'Server01' + + # Validate that the correct UserProfile object is returned + $result.SID | Should -Be 'S-1-5-21-1001' + + # Ensure that Write-Warning was not called + Assert-MockCalled Write-Warning -Exactly 0 -Scope It + } + } + } + + # Test: Profile not found, valid SID format + Context 'When the profile is not found but the SID is valid' { + It 'Should return a ProfileDeletionResult indicating failure and log a warning' { + InModuleScope -ScriptBlock { + # Mock audit results with no matching SID + $mockAuditResults = @() + + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-1002' -ProfilePath $Null -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock Validate-SIDFormat to return true (valid SID format) + Mock Validate-SIDFormat { return $true } + + # Call the function with a valid but non-existent SID + $result = Resolve-UserProfileForDeletion -SID 'S-1-5-21-1001' -AuditResults $mockAuditResults -ComputerName 'Server01' + + # Validate that the ProfileDeletionResult indicates failure + $result.SID | Should -Be 'S-1-5-21-1001' + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be 'Profile not found' + + # Ensure that Write-Warning was called with the correct message + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { + $Message -eq 'Profile not found for SID: S-1-5-21-1001 on Server01.' + } + } + } + } + + # Test: Invalid SID format + Context 'When the SID format is invalid' { + It 'Should return a ProfileDeletionResult indicating invalid SID and log a warning' { + InModuleScope -ScriptBlock { + # Mock audit results + $mockAuditResults = @() + + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-1002' -ProfilePath $Null -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock Validate-SIDFormat to return false (invalid SID format) + Mock Validate-SIDFormat { return $false } + + + # Call the function with an invalid SID format + $result = Resolve-UserProfileForDeletion -SID 'Invalid-SID' -AuditResults $mockAuditResults -ComputerName 'Server01' + + # Validate that the ProfileDeletionResult indicates failure due to invalid SID + $result.SID | Should -Be 'Invalid-SID' + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be 'Invalid SID format encountered' + + # Ensure that Write-Warning was called with the correct message + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { + $Message -eq 'Invalid SID format encountered: Invalid-SID on Server01.' + } + } + } + } + + # Test: No matching profile and invalid SID format + Context 'When the SID format is invalid and profile is not found' { + It 'Should log an appropriate warning and return a failure result' { + InModuleScope -ScriptBlock { + # Mock Validate-SIDFormat to return false + Mock Validate-SIDFormat { return $false } + + $mockAuditResults = @() + + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-1002' -ProfilePath $Null -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Call the function with an invalid SID + $result = Resolve-UserProfileForDeletion -SID 'Invalid-SID' -AuditResults $mockAuditResults -ComputerName 'Server01' + + # Validate that the ProfileDeletionResult indicates failure + $result.SID | Should -Be 'Invalid-SID' + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be 'Invalid SID format encountered' + + # Ensure that Write-Warning was called with the correct message + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { + $Message -eq 'Invalid SID format encountered: Invalid-SID on Server01.' + } + } + } + } + + # Test: No profiles in the audit results + Context 'When there are no profiles in the audit results' { + It 'Should return a ProfileDeletionResult indicating failure and log a warning' { + InModuleScope -ScriptBlock { + # Mock an empty audit results array + $mockAuditResults = @() + + # Mock Validate-SIDFormat to return true + Mock Validate-SIDFormat { return $true } + + + # Call the function with no profiles in the audit results + $result = Resolve-UserProfileForDeletion -SID 'S-1-5-21-1001' -AuditResults $mockAuditResults -ComputerName 'Server01' + + # Validate that the ProfileDeletionResult indicates failure + $result.SID | Should -Be 'S-1-5-21-1001' + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be 'Profile not found' + + # Ensure that Write-Warning was called with the correct message + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { + $Message -eq 'Profile not found for SID: S-1-5-21-1001 on Server01.' + } + } + } + } +} diff --git a/tests/Unit/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.tests.ps1 new file mode 100644 index 0000000..585bafb --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.tests.ps1 @@ -0,0 +1,171 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Resolve-UsernamesToSIDs' -Tags 'Private', 'RemoveProfileReg' { + + BeforeEach { + InModuleScope -ScriptBlock { + # Mocking the Get-SIDFromUsername function + Mock -CommandName Get-SIDFromUsername + Mock -CommandName Write-Warning + } + } + + # Test: Resolving valid usernames to SIDs + Context 'When all usernames are valid' { + It 'Should return an array of corresponding SIDs' { + InModuleScope -ScriptBlock { + # Mock Get-SIDFromUsername to return valid SIDs + Mock Get-SIDFromUsername { + param($Username, $ComputerName) + switch ($Username) + { + 'user1' + { + return 'S-1-5-21-1001' + } + 'user2' + { + return 'S-1-5-21-1002' + } + } + } + + # Call the function with valid usernames + $result = Resolve-UsernamesToSIDs -Usernames 'user1', 'user2' -ComputerName 'Server01' + + # Validate that the returned array contains the correct SIDs + $result | Should -Be @('S-1-5-21-1001', 'S-1-5-21-1002') + + # Ensure that Write-Warning was not called + Assert-MockCalled Write-Warning -Exactly 0 -Scope It + } + } + } + + # Test: Resolving usernames with some unresolved + Context 'When some usernames cannot be resolved' { + It 'Should return SIDs for valid usernames and log warnings for unresolved ones' { + InModuleScope -ScriptBlock { + # Mock Get-SIDFromUsername to return SIDs for some usernames and $null for others + Mock Get-SIDFromUsername { + param($Username, $ComputerName) + switch ($Username) + { + 'user1' + { + return 'S-1-5-21-1001' + } + 'invalidUser' + { + return $null + } + } + } + + # Call the function with a mix of valid and invalid usernames + $result = Resolve-UsernamesToSIDs -Usernames 'user1', 'invalidUser' -ComputerName 'Server01' + + # Validate that the returned array contains only the valid SID + $result | Should -Be @('S-1-5-21-1001') + + # Ensure that Write-Warning was called for the unresolved username + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { + $Message -eq 'Could not resolve SID for username invalidUser on Server01.' + } + } + } + } + + # Test: Resolving no usernames (empty input) + Context 'When no usernames are provided' { + It 'Should return an empty array' { + InModuleScope -ScriptBlock { + # Call the function with an empty array of usernames + $result = Resolve-UsernamesToSIDs -Usernames @() -ComputerName 'Server01' + + # Validate that the result is an empty array + $result | Should -Be @() + + # Ensure that Get-SIDFromUsername and Write-Warning were not called + Assert-MockCalled Get-SIDFromUsername -Exactly 0 -Scope It + Assert-MockCalled Write-Warning -Exactly 0 -Scope It + } + } + } + + # Test: Resolving multiple usernames + Context 'When resolving multiple usernames' { + It 'Should return the correct SIDs for each username' { + InModuleScope -ScriptBlock { + # Mock Get-SIDFromUsername to return SIDs for multiple usernames + Mock Get-SIDFromUsername { + param($Username, $ComputerName) + switch ($Username) + { + 'user1' + { + return 'S-1-5-21-1001' + } + 'user2' + { + return 'S-1-5-21-1002' + } + 'user3' + { + return 'S-1-5-21-1003' + } + } + } + + # Call the function with multiple usernames + $result = Resolve-UsernamesToSIDs -Usernames 'user1', 'user2', 'user3' -ComputerName 'Server01' + + # Validate that the returned array contains the correct SIDs + $result | Should -Be @('S-1-5-21-1001', 'S-1-5-21-1002', 'S-1-5-21-1003') + + # Ensure that Write-Warning was not called + Assert-MockCalled Write-Warning -Exactly 0 -Scope It + } + } + } + + # Test: Unresolved usernames result in warning + Context 'When a username cannot be resolved' { + It 'Should log a warning for unresolved usernames' { + InModuleScope -ScriptBlock { + # Mock Get-SIDFromUsername to return $null for unresolved usernames + Mock Get-SIDFromUsername { + return $null + } + + # Call the function with an unresolved username + $result = Resolve-UsernamesToSIDs -Usernames 'invalidUser' -ComputerName 'Server01' + + # Validate that the result is an empty array + $result | Should -Be @() + + # Ensure that Write-Warning was called with the correct message + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { + $Message -eq 'Could not resolve SID for username invalidUser on Server01.' + } + } + } + } +} diff --git a/tests/Unit/Private/Test-FolderExists.tests.ps1 b/tests/Unit/Private/ValidateFunctions/Test-FolderExists.tests.ps1 similarity index 100% rename from tests/Unit/Private/Test-FolderExists.tests.ps1 rename to tests/Unit/Private/ValidateFunctions/Test-FolderExists.tests.ps1 diff --git a/tests/Unit/Private/Test-OrphanedProfile.tests.ps1 b/tests/Unit/Private/ValidateFunctions/Test-OrphanedProfile.tests.ps1 similarity index 100% rename from tests/Unit/Private/Test-OrphanedProfile.tests.ps1 rename to tests/Unit/Private/ValidateFunctions/Test-OrphanedProfile.tests.ps1 diff --git a/tests/Unit/Private/Test-SpecialAccount.tests.ps1 b/tests/Unit/Private/ValidateFunctions/Test-SpecialAccount.tests.ps1 similarity index 100% rename from tests/Unit/Private/Test-SpecialAccount.tests.ps1 rename to tests/Unit/Private/ValidateFunctions/Test-SpecialAccount.tests.ps1 diff --git a/tests/Unit/Private/Validate-SIDFormat.tests.ps1 b/tests/Unit/Private/ValidateFunctions/Validate-SIDFormat.tests.ps1 similarity index 100% rename from tests/Unit/Private/Validate-SIDFormat.tests.ps1 rename to tests/Unit/Private/ValidateFunctions/Validate-SIDFormat.tests.ps1 diff --git a/tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 b/tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 new file mode 100644 index 0000000..93edc65 --- /dev/null +++ b/tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 @@ -0,0 +1,1167 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName + + # Set up environment variables used in the function + $env:WinProfileOps_RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" + $env:WinProfileOps_RegistryHive = [Microsoft.Win32.RegistryHive]::LocalMachine + $env:WinProfileOps_RegBackUpDirectory = "C:\LHStuff\RegBackUp" + $env:WinProfileOps_ProfileFolderPath = "$env:SystemDrive\Users" +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Remove-UserProfilesFromRegistry' -Tag 'Public' { + + # Mock the environment variables and helper functions + BeforeEach { + + InModuleScope -ScriptBlock { + + #Mock -CommandName Test-EnvironmentVariable -MockWith { return 'SomePath' } + Mock -CommandName Invoke-UserProfileRegRemoval { + + } + + Mock -CommandName 'PromptForConfirmation' -MockWith { + param ($ComputerName, $ItemCount, $AuditOnly, $Confirm) + return $Confirm + } + + Mock -CommandName Get-SIDFromUsername -MockWith { + param($Username, $ComputerName) + + # Simulating behavior: Return SIDs for known users, $null for unknown users + switch ($Username) + { + 'testuser1' + { + return 'S-1-5-21-1234567890-123456789-123456789-1001' + } + 'testuser2' + { + return 'S-1-5-21-1234567890-123456789-123456789-1002' + } + default + { + return $null + } # Simulate unresolved users + } + } + + Mock -CommandName Resolve-UsernamesToSIDs -MockWith { + param($Usernames, $ComputerName) + + $SIDs = $Usernames | ForEach-Object { + Get-SIDFromUsername -Username $_ -ComputerName $ComputerName + } + + return $SIDs + } + + #Mock -CommandName 'PromptForConfirmation' -MockWith { return $true } + + Mock -CommandName "ShouldContinueWrapper" -MockWith { } + + Mock Invoke-UserProfileAudit { + param($IgnoreSpecial, $computerName) + + $objects = @() + $objects += New-UserProfileObject -SID "S-1-5-21-1234567890-1003" -ProfilePath "$env:SystemDrive\Users\TestUserSpecial" -IsOrphaned $false -ComputerName $computerName -IsSpecial $true + $objects += New-UserProfileObject -SID "S-1-5-21-1234567890-1001" -ProfilePath "$env:SystemDrive\Users\TestUser1" -IsOrphaned $false -ComputerName $computerName -IsSpecial $false + $objects += New-UserProfileObject -SID "S-1-5-21-1234567890-1002" -ProfilePath "$env:SystemDrive\Users\TestUser2" -IsOrphaned $false -ComputerName $computerName -IsSpecial $false + if ($IgnoreSpecial) + { + return $objects | Where-Object { $_.IsSpecial -eq $false } + } + else + { + return $objects + } + } + + } -ModuleName $script:dscModuleName + } + + + ### Input Tests ### + Context 'Using SIDSet' { + It 'Should call Invoke-UserProfileRegRemoval when using SIDSet' { + $SIDs = @('S-1-5-21-1234567890-123456789-123456789-1001') + $computerName = 'TestComputer' + + # Run the function + $Return = Remove-UserProfilesFromRegistry -SIDs $SIDs -ComputerName $computerName -Force + + # Assert that Invoke-UserProfileRegRemoval is called with the correct parameters + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SIDs -contains 'S-1-5-21-1234567890-123456789-123456789-1001' + } + } + + It 'Should call Invoke-UserProfileRegRemoval in audit mode when using SIDSet' { + $SIDs = @('S-1-5-21-1234567890-123456789-123456789-1001') + $computerName = 'TestComputer' + + # Run the function + $Return = Remove-UserProfilesFromRegistry -SIDs $SIDs -AuditOnly -ComputerName $computerName -Force + + # Assert that Invoke-UserProfileRegRemoval is called in audit mode + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $AuditOnly -eq $true -and $SIDs -contains 'S-1-5-21-1234567890-123456789-123456789-1001' + } + } + + It 'Should process multiple SIDs correctly' { + InModuleScope -ScriptBlock { + + $SIDs = @('S-1-5-21-1234567890-123456789-123456789-1001', 'S-1-5-21-1234567890-123456789-123456789-1002') + $computerName = 'TestComputer' + + # Run the function + Remove-UserProfilesFromRegistry -SIDs $SIDs -ComputerName $computerName -Force + + # Assert that Invoke-UserProfileRegRemoval is called for both SIDs + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It -ParameterFilter { + $SIDs -contains 'S-1-5-21-1234567890-123456789-123456789-1001' -or $SIDs -contains 'S-1-5-21-1234567890-123456789-123456789-1002' -and $ComputerName -eq 'TestComputer' + } + + } + } + + It 'Should prompt for confirmation with Confirm:$true even if Force:$true' { + InModuleScope -ScriptBlock { + + $SIDs = @('S-1-5-21-1234567890-123456789-123456789-1001') + $computerName = 'TestComputer' + + # Mock ShouldContinueWrapper to simulate confirmation prompt + Mock -CommandName 'ShouldContinueWrapper' -MockWith { return $true } + + # Mock the confirmation prompt for both computers + Mock -CommandName 'PromptForConfirmation' -MockWith { + return ShouldContinueWrapper -Context $PSCmdlet -QueryMessage "Are you sure?" -CaptionMessage "Confirm Deletion" + } + # Run the function with Confirm enabled and Force true + Remove-UserProfilesFromRegistry -SIDs $SIDs -ComputerName $computerName -Force:$true -Confirm:$true + + # Assert that PromptForConfirmation was called + Assert-MockCalled -CommandName 'ShouldContinueWrapper' -Exactly -Times 1 -Scope It + + # Assert that Invoke-UserProfileRegRemoval was called after confirmation + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -Scope It -ParameterFilter { + $Confirm -eq $true -and $Force -eq $true -and $ComputerName -eq 'TestComputer' + } + } + } + + It 'Should bypass confirmation with Confirm:$false and Force:$true' { + InModuleScope -ScriptBlock { + + $SIDs = @('S-1-5-21-1234567890-123456789-123456789-1001') + $computerName = 'TestComputer' + + Mock -CommandName 'PromptForConfirmation' -MockWith { + return $true + } + + # Run the function with Confirm disabled and Force true + Remove-UserProfilesFromRegistry -SIDs $SIDs -ComputerName $computerName -Force:$true -Confirm:$false + + + # Assert that PromptForConfirmation was not called + Assert-MockCalled -CommandName 'ShouldContinueWrapper' -Exactly -Times 0 -Scope It + + # Assert that Invoke-UserProfileRegRemoval was called after bypassing confirmation + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -Scope It -ParameterFilter { + $Confirm -eq $false -and $Force -eq $true -and $ComputerName -eq 'TestComputer' + } + } + } + + It 'Should default to local computer if ComputerName is not provided' { + InModuleScope -ScriptBlock { + + $SIDs = @('S-1-5-21-1234567890-123456789-123456789-1001') + + # Run the function without specifying ComputerName + Remove-UserProfilesFromRegistry -SIDs $SIDs -Force + + # Assert that Invoke-UserProfileRegRemoval is called with local computer name + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq $env:COMPUTERNAME + } + } + } + + + } + + Context 'Using UserNameSet' { + It 'Should resolve usernames to SIDs and call Invoke-UserProfileRegRemoval' { + $Usernames = @('testuser1', 'testuser2') + $computerName = 'TestComputer' + $ExpectedSIDs = @('S-1-5-21-1234567890-123456789-123456789-1001', 'S-1-5-21-1234567890-123456789-123456789-1002') + + # Run the function + $return = Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force + + # Assert that Resolve-UsernamesToSIDs was called + should -Invoke -CommandName Resolve-UsernamesToSIDs -Exactly -Times 1 -Scope It -ParameterFilter { + $Usernames -contains 'testuser1' -and $Usernames -contains 'testuser2' + } + + # Assert that Invoke-UserProfileRegRemoval is called with resolved SIDs + should -Invoke -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SID -eq 'S-1-5-21-1234567890-123456789-123456789-1001' + } + + should -Invoke -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SID -eq 'S-1-5-21-1234567890-123456789-123456789-1002' + } + + should -Invoke -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It + + } + + It 'Should resolve usernames to SIDs and audit profiles without deletion' { + $Usernames = @('testuser1', 'testuser2') + $computerName = 'TestComputer' + $ExpectedSIDs = @('S-1-5-21-1234567890-123456789-123456789-1001', 'S-1-5-21-1234567890-123456789-123456789-1002') + + # Run the function + $return = Remove-UserProfilesFromRegistry -Usernames $Usernames -AuditOnly -ComputerName $computerName -Force + + # Assert that Resolve-UsernamesToSIDs was called + should -Invoke -CommandName Resolve-UsernamesToSIDs -Exactly -Times 1 -Scope It -ParameterFilter { + $Usernames -contains 'testuser1' -and $Usernames -contains 'testuser2' + } + + # Assert that Invoke-UserProfileRegRemoval was called in audit mode + should -Invoke -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $AuditOnly -eq $true -and $SID -eq 'S-1-5-21-1234567890-123456789-123456789-1001' + } + } + + It 'Should Call Resolve-UserNamesToSIDs when Usernames are provided' { + $Usernames = @('testuser1', 'testuser2') + $computerName = 'TestComputer' + $ExpectedSIDs = @('S-1-5-21-1234567890-123456789-123456789-1001', 'S-1-5-21-1234567890-123456789-123456789-1002') + + # Run the function + $return = Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force + + # Assert that Resolve-UsernamesToSIDs was called + should -Invoke -CommandName Resolve-UsernamesToSIDs -Exactly -Times 1 -Scope It -ParameterFilter { + $Usernames -contains 'testuser1' -and $Usernames -contains 'testuser2' + } + } + + + It 'Should handle partial resolution of usernames' { + $Usernames = @('testuser1', 'unknownuser') + $computerName = 'TestComputer' + $ExpectedSIDs = @('S-1-5-21-1234567890-123456789-123456789-1001') + + # Run the function + $return = Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force + + # Assert that Resolve-UsernamesToSIDs was called + should -Invoke -CommandName Resolve-UsernamesToSIDs -Exactly -Times 1 + + # Assert that Invoke-UserProfileRegRemoval is called with resolved SIDs only + should -Invoke -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SID -eq 'S-1-5-21-1234567890-123456789-123456789-1001' + } + + + should -Invoke -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 0 -Scope It -ParameterFilter { + $SID -eq $null + } + + } + + It 'Should handle invalid usernames gracefully' { + $Usernames = @('invaliduser1', 'invaliduser2') + $computerName = 'TestComputer' + + # Mock Resolve-UsernamesToSIDs to return $null for invalid users + Mock -CommandName Resolve-UsernamesToSIDs -MockWith { return @() } + + # Run the function + $return = Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force -ErrorAction SilentlyContinue + + # Assert that Resolve-UsernamesToSIDs was called and returned $null + Assert-MockCalled -CommandName Resolve-UsernamesToSIDs -Exactly -Times 1 -Scope It + + # Assert that Invoke-UserProfileRegRemoval was not called since no SIDs were resolved + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 0 -Scope It + } + + It 'Should throw an error if an empty username array is provided' { + $Usernames = @() + $computerName = 'TestComputer' + + # Run the function with an empty username array and ensure it throws + { Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force } | Should -Throw + } + + It 'Should prompt for confirmation with Confirm:$true even if Force:$true' { + InModuleScope -ScriptBlock { + $Usernames = @('testuser1') + $computerName = 'TestComputer' + # Mock ShouldContinueWrapper to simulate confirmation prompt + Mock -CommandName 'ShouldContinueWrapper' -MockWith { return $true } + + # Mock the confirmation prompt + Mock -CommandName 'PromptForConfirmation' -MockWith { + return ShouldContinueWrapper -Context $PSCmdlet -QueryMessage "Are you sure?" -CaptionMessage "Confirm Deletion" + } + + # Run the function with Confirm enabled and Force true + Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force:$true -Confirm:$true + + # Assert that PromptForConfirmation was called + Assert-MockCalled -CommandName 'PromptForConfirmation' -Exactly -Times 1 -Scope It + + # Assert that Invoke-UserProfileRegRemoval was called after confirmation + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -Scope It + + } + } + + It 'Should bypass confirmation with Confirm:$false and Force:$true' { + + InModuleScope -ScriptBlock { + + $Usernames = @('testuser1') + $computerName = 'TestComputer' + + # Mock the confirmation prompt to ensure it's not called + Mock -CommandName 'PromptForConfirmation' -MockWith { + param($ComputerName, $ItemCount, $AuditOnly, $Confirm, $context) + if ($confirm) + { + return ShouldContinueWrapper -Context $context -QueryMessage "Are you sure?" -CaptionMessage "Confirm Deletion" + } + else + { + return $true + } + } + + # Run the function with Confirm disabled and Force true + Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force:$true -Confirm:$false + + # Assert that PromptForConfirmation was not called + Assert-MockCalled -CommandName 'PromptForConfirmation' -Exactly -Times 1 -Scope It + + # Assert that PromptForConfirmation was called + Assert-MockCalled -CommandName 'ShouldContinueWrapper' -Exactly -Times 0 -Scope It + + # Assert that Invoke-UserProfileRegRemoval was called after bypassing confirmation + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -Scope It + + } + } + + + It 'Should default to local computer if ComputerName is not provided' { + $Usernames = @('testuser1') + + # Run the function without specifying ComputerName + Remove-UserProfilesFromRegistry -Usernames $Usernames -Force + + # Assert that Invoke-UserProfileRegRemoval is called with local computer name + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq $env:COMPUTERNAME + } + } + + + + } + + Context 'Using UserProfileSet' { + + It 'Should prompt for confirmation before deleting profiles' { + InModuleScope -ScriptBlock { + + # Create mock user profile objects + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + # Mock ShouldContinueWrapper to simulate user saying "yes" + Mock -CommandName 'ShouldContinueWrapper' -MockWith { return $true } + + # Mock the confirmation prompt (simulating that confirmation happens) + Mock -CommandName 'PromptForConfirmation' -MockWith { + # Ensure ShouldContinueWrapper is triggered when Confirm is true + return ShouldContinueWrapper -Context $PSCmdlet -QueryMessage "Are you sure?" -CaptionMessage "Confirm Deletion" + } + + # Run the function with Confirm enabled + Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Confirm:$true + + # Assert that PromptForConfirmation was called once + Assert-MockCalled -CommandName 'PromptForConfirmation' -Exactly -Times 1 -Scope It + + # Assert that ShouldContinueWrapper was called once as part of confirmation + Assert-MockCalled -CommandName 'ShouldContinueWrapper' -Exactly -Times 1 -Scope It + + # Assert that Invoke-UserProfileRegRemoval is called after confirmation + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -ParameterFilter { + $Confirm -eq $true -and $Force -eq $false + } + + } + } + + + It 'Should bypass confirmation and delete profiles with -Force -Confirm:$false' { + InModuleScope -ScriptBlock { + + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + Mock -CommandName 'PromptForConfirmation' -MockWith { + param($ComputerName, $ItemCount, $AuditOnly, $Confirm, $context) + if ($confirm) + { + return ShouldContinueWrapper -Context $context -QueryMessage "Are you sure?" -CaptionMessage "Confirm Deletion" + } + else + { + return $true + } + } + + # Run the function with Confirm enabled + Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force:$true -Confirm:$false + + # Assert that PromptForConfirmation was called + Assert-MockCalled -CommandName 'ShouldContinueWrapper' -Exactly -Times 0 -Scope It + + # Assert that Invoke-UserProfileRegRemoval is called after confirmation + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 + + } + } + + + It 'Should call Invoke-UserProfileRegRemoval with UserProfiles' { + InModuleScope -ScriptBlock { + # Create mock UserProfile objects using New-UserProfileObject + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force + + $GroupedProfiles = $MockUserProfileObjects | Group-Object -Property ComputerName + + $Profile1 = ($GroupedProfiles.Group)[0] + $Profile2 = ($GroupedProfiles.Group)[1] + + + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It + + # Assert that Invoke-UserProfileRegRemoval is called with the UserProfiles + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SID -eq $Profile1.SID -and $computerName -eq $Profile1.computerName + } + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SID -eq $Profile2.SID -and $computerName -eq $Profile2.computerName + } + + } + } + + It 'Should call Invoke-UserProfileRegRemoval in audit mode with UserProfiles' { + + InModuleScope -ScriptBlock { + # Create mock UserProfile objects using New-UserProfileObject + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + # Run the function + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -AuditOnly + + $GroupedProfiles = $MockUserProfileObjects | Group-Object -Property ComputerName + + $Profile1 = ($GroupedProfiles.Group)[0] + $Profile2 = ($GroupedProfiles.Group)[1] + + + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It -ParameterFilter { + $AuditOnly -eq $true + } + + # Assert that Invoke-UserProfileRegRemoval is called with the UserProfiles + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SID -eq $Profile1.SID -and $computerName -eq $Profile1.computerName -and $AuditOnly -eq $true + } + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SID -eq $Profile2.SID -and $computerName -eq $Profile2.computerName + } + + } + } + + It 'Should group UserProfiles by ComputerName and call processing per group' { + InModuleScope -ScriptBlock { + # Create mock UserProfile objects using New-UserProfileObject + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer2' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force + + # Assert that Invoke-UserProfileRegRemoval is called for each computer + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq 'TestComputer1' + } + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq 'TestComputer2' + } + + + } + } + + It 'Should process profiles grouped by ComputerName' { + InModuleScope -ScriptBlock { + # Create mock UserProfile objects for different computers + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer2' -IsSpecial:$false + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force + + # Assert that Invoke-UserProfileRegRemoval is called for each computer + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq 'TestComputer1' + } + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq 'TestComputer2' + } + } + } + + It 'Should prompt and process profiles for each unique computer' { + InModuleScope -ScriptBlock { + + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer2' -IsSpecial:$false + + # Mock ShouldContinueWrapper to simulate the prompt and user saying "yes" + Mock -CommandName 'ShouldContinueWrapper' -MockWith { return $true } + + # Mock the confirmation prompt for both computers + Mock -CommandName 'PromptForConfirmation' -MockWith { + return ShouldContinueWrapper -Context $PSCmdlet -QueryMessage "Are you sure?" -CaptionMessage "Confirm Deletion" + } + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Confirm:$true + + # Assert that confirmation prompt was shown for both computers + Assert-MockCalled -CommandName 'PromptForConfirmation' -Exactly -Times 2 -Scope It + + # Assert that profiles were processed for both computers + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 2 -ParameterFilter { + $Computername -eq 'TestComputer1' -or $Computername -eq 'TestComputer2' + } + } + } + + It 'Should process single profile for a computer correctly' { + InModuleScope -ScriptBlock { + # Create mock UserProfile object for one computer + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force + + # Assert that Invoke-UserProfileRegRemoval is called once for the computer + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq 'TestComputer1' + } + } + } + } + + + ### Mode Tests ### + Context 'Audit Mode' { + It 'Should return profiles for audit and not remove them' { + InModuleScope -ScriptBlock { + # Create mock UserProfile objects for auditing + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + + + Mock -CommandName Invoke-UserProfileRegRemoval { + param($SID, $ComputerName, $AuditOnly, $Force) + + # Prepare the deletion result parameters + $deletionResultParams = @{ + SID = $SID + ProfilePath = "FakeProfilePath" + ComputerName = $ComputerName + DeletionSuccess = $false + DeletionMessage = "Profile not removed." + } + + # If in audit mode, output an audit-only result directly to the pipeline and return + if ($AuditOnly) + { + $deletionResultParams.DeletionSuccess = $true + $deletionResultParams.DeletionMessage = "Audit only, no deletion performed." + New-ProfileDeletionResult @deletionResultParams + } + + } + + # Run the function in -AuditOnly mode + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -AuditOnly + + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It -ParameterFilter { + $AuditOnly -eq $true -and $force -eq $false + } + + # Assert that profiles were audited but not removed + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $AuditOnly -eq $true -and $SID -eq 'S-1-5-21-1234567890-1001' + } + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $AuditOnly -eq $true -and $SID -eq 'S-1-5-21-1234567890-1002' + } + + # Assert that no profiles were actually removed (simulate no action in Audit mode) + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 0 -Scope It -ParameterFilter { + $Force -eq $true + } + + # Assert that the function returned the correct profiles for auditing + foreach ($item in $return) + { + $item.GetType().Name | Should -Be 'ProfileDeletionResult' + } + $return.Count | Should -Be $MockUserProfileObjects.Count + } + } + + It 'Should throw an error when empty lists are provided' { + InModuleScope -ScriptBlock { + $EmptySIDs = @() + $EmptyUsernames = @() + $EmptyUserProfiles = @() + + # Run the function with empty SIDs + { Remove-UserProfilesFromRegistry -SIDs $EmptySIDs -AuditOnly -Force } | Should -Throw + + # Run the function with empty Usernames + { Remove-UserProfilesFromRegistry -Usernames $EmptyUsernames -AuditOnly -Force } | Should -Throw + + # Run the function with empty UserProfiles + { Remove-UserProfilesFromRegistry -UserProfiles $EmptyUserProfiles -AuditOnly -Force } | Should -Throw + } + } + + It 'Should bypass deletion and only audit when AuditOnly is set' { + InModuleScope -ScriptBlock { + + # Create mock user profile objects + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + # Mock Invoke-UserProfileRegRemoval to simulate audit mode + Mock -CommandName 'Invoke-UserProfileRegRemoval' -MockWith { + param($SID, $ComputerName, $AuditOnly, $Force) + New-ProfileDeletionResult -SID $SID -ProfilePath "FakePath" -DeletionSuccess $false -DeletionMessage "Audit mode" -ComputerName $ComputerName + } + + # Run the function in audit mode + $Return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -AuditOnly + + # Assert that no deletion was attempted + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -ParameterFilter { + $AuditOnly -eq $true + } + + # Ensure the returned result contains audit information + $Return[0].DeletionMessage | Should -Be 'Audit mode' + } + } + } + + ### Error Handling ### + Context "Error Tests" { + + It 'Should throw error if no SIDs, Usernames, or UserProfiles are provided' { + $computerName = "TestComputer" + { Remove-UserProfilesFromRegistry -ComputerName $computerName -Force } | Should -Throw + } + + It 'Should throw empty SIDs array' { + + $computerName = "TestComputer" + $SIDs = @() + { Remove-UserProfilesFromRegistry -SIDs $SIDs -ComputerName $computerName -Force } | Should -Throw + } + + It 'Should throw empty Usernames array' { + $computerName = "TestComputer" + $Usernames = @() + { Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force } | Should -Throw + } + + It 'Should throw empty UserProfiles array' { + $computerName = "TestComputer" + $UserProfiles = @() + { Remove-UserProfilesFromRegistry -UserProfiles $UserProfiles -ComputerName $computerName -Force } | Should -Throw + } + + It 'Should handle exceptions thrown during profile removal' { + InModuleScope -ScriptBlock { + # Mock UserProfile objects + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser' -IsOrphaned $false -ComputerName 'TestComputer' -IsSpecial:$false + + mock Write-Error + + # Mock Invoke-UserProfileRegRemoval to throw an error + Mock -CommandName Invoke-UserProfileRegRemoval -MockWith { throw "Test exception" } + + # Run the function and catch the error + { Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force } | Should -not -Throw + + + # Assert that Write-Error was called + Assert-MockCalled -CommandName Write-Error -Exactly -Times 1 -Scope It + + + # Optionally, check if the function handled the error gracefully + # Assert that an appropriate error message is logged or returned + } + } + + It 'Should throw an error if no UserProfiles, SIDs, or Usernames are provided' { + $computerName = "TestComputer" + { Remove-UserProfilesFromRegistry -ComputerName $computerName -Force } | Should -Throw + } + + + } + + ### Large Input Sets ### + Context 'Large Input Sets - Maximum Profiles' { + It 'Should process a large number of profiles without errors' { + InModuleScope -ScriptBlock { + # Mock a large number of UserProfile objects + $MockUserProfileObjects = 1..100 | ForEach-Object { + New-UserProfileObject -SID "S-1-5-21-1234567890-$_" -ProfilePath "C:\Users\testuser$_" -IsOrphaned $false -ComputerName 'TestComputer' -IsSpecial:$false + } + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force + + # Assert that all profiles were processed without errors + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 100 + } + } + } + +} + + + +<# + It 'Should call Invoke-UserProfileRegRemoval in audit mode with UserProfiles' { + + InModuleScope -ScriptBlock { + # Create mock UserProfile objects using New-UserProfileObject + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force -AuditOnly + + $GroupedProfiles = $MockUserProfileObjects | Group-Object -Property ComputerName + + $ProfilesGroup = ($GroupedProfiles.Group)[0] + + + # Assert that Invoke-UserProfileRegRemoval is called with the UserProfiles + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $Profiles -contains $MockUserProfileObjects[0] ` + -and $Profiles -contains $MockUserProfileObjects[1] ` + -and $computerName -eq "TestComputer1" -and $AuditOnly -eq $true + } + + } + } + + It 'Should group UserProfiles by ComputerName and call processing per group' { + InModuleScope -ScriptBlock { + # Create mock UserProfile objects using New-UserProfileObject + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer2' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force + + # Assert that Invoke-UserProfileRegRemoval is called for each computer + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq 'TestComputer1' + } + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq 'TestComputer2' + } + + + } + } + + + } + + Context "Error Tests" { + + It 'Should throw error if no SIDs, Usernames, or UserProfiles are provided' { + { Remove-UserProfilesFromRegistry -ComputerName 'TestComputer' -Force } | Should -Throw + } + + + } + + Context "Empty Input Tests" { + + It 'Should throw empty SIDs array' { + $SIDs = @() + { Remove-UserProfilesFromRegistry -SIDs $SIDs -ComputerName 'TestComputer' -Force } | Should -Throw + } + + It 'Should throw empty Usernames array gracefully' { + $Usernames = @() + { Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName 'TestComputer' -Force } | Should -Throw + } + + It 'Should throw empty UserProfiles array gracefully' { + $UserProfiles = @() + { Remove-UserProfilesFromRegistry -UserProfiles $UserProfiles -ComputerName 'TestComputer' -Force } | Should -Throw + } + + + } + +} + + + + + BeforeEach { + + InModuleScope -scriptblock { + + # Mock necessary functions + Mock Get-DirectoryPath { "C:\LHStuff\RegBackUp" } + Mock Test-DirectoryExistence { $true } + Mock Open-RegistryKey { New-MockObject -Type Microsoft.Win32.RegistryKey -Methods @{ Dispose = {} } } + Mock Invoke-UserProfileAudit { + param($IgnoreSpecial, $computerName) + + $objects = @() + $objects += New-UserProfileObject -SID "S-1-5-21-1234567890-1003" -ProfilePath "$env:SystemDrive\Users\TestUserSpecial" -IsOrphaned $false -ComputerName $computerName -IsSpecial $true + $objects += New-UserProfileObject -SID "S-1-5-21-1234567890-1001" -ProfilePath "$env:SystemDrive\Users\TestUser1" -IsOrphaned $false -ComputerName $computerName -IsSpecial $false + $objects += New-UserProfileObject -SID "S-1-5-21-1234567890-1002" -ProfilePath "$env:SystemDrive\Users\TestUser2" -IsOrphaned $false -ComputerName $computerName -IsSpecial $false + if ($IgnoreSpecial) + { + return $objects | Where-Object { $_.IsSpecial -eq $false } + } + else + { + return $objects + } + } + + Mock Invoke-UserProfileRegRemoval { + param($ComputerName, $SIDs, $Profiles, $RegistryPath, $ProfileFolderPath, $RegistryHive, $Force, $AuditOnly, $Confirm) + + # Initialize $deletionResults as an empty array + $deletionResults = @() + + if ($SIDs) + { + foreach ($SID in $SIDs) + { + Invoke-SingleProfileAction -SID $SID -AuditResults $null -ComputerName $ComputerName -BaseKey $null -Force:$Force -AuditOnly:$AuditOnly -DeletionResults ([ref]$deletionResults) -Confirm:$Confirm + } + } + if ($Profiles) + { + foreach ($Profile in $Profiles) + { + Invoke-SingleProfileAction -SID $Profile.SID -AuditResults $null -SelectedProfile $Profile -ComputerName $ComputerName -BaseKey $null -Force:$Force -AuditOnly:$AuditOnly -DeletionResults ([ref]$deletionResults) -Confirm:$Confirm + } + } + + return $deletionResults # Return the accumulated deletion results + } + + Mock Invoke-SingleProfileAction { + param($SID, $AuditResults, $SelectedProfile, $BaseKey, [ref]$DeletionResults, $Force, $AuditOnly, $Confirm) + + # Call Invoke-ProcessProfileRemoval to simulate the removal or auditing of the profile + $results = Invoke-ProcessProfileRemoval -SID $SID -SelectedProfile $SelectedProfile -BaseKey $BaseKey -AuditOnly:$AuditOnly -ComputerName $ComputerName -Confirm:$Confirm + + # Append the result to the DeletionResults array + $DeletionResults.Value += $results + } + + Mock Invoke-ProcessProfileRemoval { + param($SID, $SelectedProfile, $BaseKey, $AuditOnly, $ComputerName, $Confirm) + + # Simulate the result based on whether AuditOnly is true or not + if ($AuditOnly) + { + return New-ProfileDeletionResult -SID $SID -ProfilePath $SelectedProfile.ProfilePath -DeletionSuccess $true -DeletionMessage "Audit only, no deletion performed." -ComputerName $ComputerName + } + else + { + return New-ProfileDeletionResult -SID $SID -ProfilePath $SelectedProfile.ProfilePath -DeletionSuccess $true -DeletionMessage "Profile removed successfully." -ComputerName $ComputerName + } + } + + } -ModuleName $Script:dscModuleName + } + + Context 'When profiles are successfully removed' { + It 'Should remove the user profile successfully' { + + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $env:COMPUTERNAME -Confirm:$false -force + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $true + $result[0].DeletionMessage | Should -Be "Profile removed successfully." + } + + It 'Should remove multiple user profiles successfully' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001", "S-1-5-21-1234567890-1002") -ComputerName $env:COMPUTERNAME -Confirm:$false -force + $result | Should -HaveCount 2 + $result[0].DeletionSuccess | Should -Be $true + $result[0].DeletionMessage | Should -Be "Profile removed successfully." + $result[1].DeletionSuccess | Should -Be $true + $result[1].DeletionMessage | Should -Be "Profile removed successfully." + } + + It 'Should only audit the profile without removing it when AuditOnly is set' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -AuditOnly -ComputerName $env:COMPUTERNAME -Confirm:$false -force + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $true + $result[0].DeletionMessage | Should -Be "Audit only, no deletion performed." + } + } + + Context 'When confirmation is required' { + It 'Should remove profile when confirmation is bypassed' { + + # Using -Confirm:$false to bypass confirmation + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -Confirm:$false -Force + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $true + + Should -Invoke Invoke-ProcessProfileRemoval -Exactly 1 -Scope It + } + } + + Context 'When profiles are successfully removed' { + It 'Should remove the user profile successfully' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $true + $result[0].DeletionMessage | Should -Be "Profile removed successfully." + } + + It 'Should remove multiple user profiles successfully' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001", "S-1-5-21-1234567890-1002") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 2 + $result[0].DeletionSuccess | Should -Be $true + $result[0].DeletionMessage | Should -Be "Profile removed successfully." + $result[1].DeletionSuccess | Should -Be $true + $result[1].DeletionMessage | Should -Be "Profile removed successfully." + } + + It 'Should only audit the profile without removing it when AuditOnly is set' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -AuditOnly -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $true + $result[0].DeletionMessage | Should -Be "Audit only, no deletion performed." + } + + It 'Should skip special profiles' { + # Mock a special profile + + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1003") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $false + $result[0].DeletionMessage | Should -Be "Profile not found." + } + + It 'Should handle profiles that are already removed or not found' { + # Mock no profile found + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1005") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $false + $result[0].DeletionMessage | Should -Be "Profile not found." + } + + + + } + + Context 'When registry or file operations fail' { + It 'Should throw an error when registry key cannot be opened' { + # Mock registry key failure + Mock Open-RegistryKey { $null } -ModuleName $script:dscModuleName + + $message = 'Error in Begin block: Failed to open registry key at path: SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' + + { Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $env:COMPUTERNAME -Confirm:$false } | Should -Throw $message + } + + It 'Should throw an error when backup directory does not exist' { + # Mock directory existence check to fail + Mock Test-DirectoryExistence { throw } -ModuleName $Script:dscModuleName + + $message = 'Error in Begin block: ScriptHalted' + { Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $env:COMPUTERNAME -Confirm:$false } | Should -Throw $message + } + } + + Context 'When confirmation is required' { + It 'Should prompt for confirmation before removing profile' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -whatif + $result | Should -BeNullOrEmpty + Should -Invoke Invoke-ProcessProfileRemoval -Exactly 0 -Scope It + } + + It 'Should remove profile when confirmation is bypassed' { + # Using -Confirm:$false to bypass confirmation + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $true + + Should -Invoke Invoke-ProcessProfileRemoval -Exactly 1 -Scope It + } + } + + Context 'Handling multiple SIDs with mixed outcomes' { + It 'Should handle a mix of profiles where some are successfully removed and some are not found' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001", "S-1-5-21-1234567890-1005") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 2 + + $result[0].DeletionSuccess | Should -Be $true + $result[0].DeletionMessage | Should -Be "Profile removed successfully." + + $result[1].DeletionSuccess | Should -Be $false + $result[1].DeletionMessage | Should -Be "Profile not found." + } + } + + Context 'When removing profiles from a remote computer' { + It 'Should remove profile from remote computer successfully' { + $remoteComputerName = "RemotePC" + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $remoteComputerName -Confirm:$false + $result | Should -HaveCount 1 + $result[0].ComputerName | Should -Be $remoteComputerName + $result[0].DeletionSuccess | Should -Be $true + } + + It 'Should handle failure when connecting to remote computer' { + Mock Open-RegistryKey { throw } + + $remoteComputerName = "RemotePC" + $message = 'Error in Begin block: ScriptHalted' + { Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $remoteComputerName -Confirm:$false } | Should -Throw $message + } + } + Context 'Handling invalid input and SIDs' { + It 'Should throw an error when no SIDs are provided' { + $message = "Cannot bind argument to parameter 'SIDs' because it is an empty array." + { Remove-UserProfilesFromRegistry -SIDs @() -ComputerName $env:COMPUTERNAME -Confirm:$false } | Should -Throw $message + } + + It 'Should return a message for an invalid SID format' { + # Simulate an invalid SID format + $result = Remove-UserProfilesFromRegistry -SIDs @("Invalid-SID") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $false + $result[0].DeletionMessage | Should -Be "Invalid SID format encountered: 'Invalid-SID'." + } + + It 'Should return a profile not found message for a valid but non-existent SID' { + # Mock no profile found for the given valid SID + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1005") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $false + $result[0].DeletionMessage | Should -Be "Profile not found." + } + + It 'Should handle multiple SIDs with one invalid and one valid SID' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-10015", "Invalid-SID") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 2 + + # The first SID should be valid but not found + $result[0].SID | Should -Be "S-1-5-21-1234567890-10015" + $result[0].DeletionSuccess | Should -Be $false + $result[0].DeletionMessage | Should -Be "Profile not found." + + # The second SID should be invalid + $result[1].SID | Should -Be "Invalid-SID" + $result[1].DeletionSuccess | Should -Be $false + $result[1].DeletionMessage | Should -Be "Invalid SID format encountered: 'Invalid-SID'." + } + } +} +#>