From ddcd7e8e95d0434c135e6ef5f0c6285e634888ae Mon Sep 17 00:00:00 2001 From: Mariah Breakey Date: Thu, 15 Dec 2016 14:03:39 -0800 Subject: [PATCH] adding all service updates --- .../en-US/MSFT_ServiceResource.schema.mfl | Bin 2810 -> 2812 bytes Examples/Sample_Service_CreateService.ps1 | 68 +- Examples/Sample_Service_DeleteService.ps1 | 60 +- .../Sample_Service_ServiceWithCredential.ps1 | 68 - ...e_Service_UpdateStartupTypeIgnoreState.ps1 | 28 + README.md | 11 +- ...MSFT_ServiceResource.Integration.Tests.ps1 | 529 ++- ...iceResource_AllExceptCredential.config.ps1 | 89 + ..._ServiceResource_CredentialOnly.config.ps1 | 39 + .../MSFT_ServiceResource_add.config.ps1 | 27 - .../MSFT_ServiceResource_edit.config.ps1 | 27 - .../MSFT_ServiceResource_remove.config.ps1 | 15 - Tests/TestHelpers/CommonTestHelper.psm1 | 3 +- .../MSFT_ServiceResource.TestHelper.psm1 | 181 +- Tests/Unit/MSFT_ServiceResource.Tests.ps1 | 4178 ++++++++++------- 15 files changed, 3010 insertions(+), 2313 deletions(-) delete mode 100644 Examples/Sample_Service_ServiceWithCredential.ps1 create mode 100644 Examples/Sample_Service_UpdateStartupTypeIgnoreState.ps1 create mode 100644 Tests/Integration/MSFT_ServiceResource_AllExceptCredential.config.ps1 create mode 100644 Tests/Integration/MSFT_ServiceResource_CredentialOnly.config.ps1 delete mode 100644 Tests/Integration/MSFT_ServiceResource_add.config.ps1 delete mode 100644 Tests/Integration/MSFT_ServiceResource_edit.config.ps1 delete mode 100644 Tests/Integration/MSFT_ServiceResource_remove.config.ps1 diff --git a/DscResources/MSFT_ServiceResource/en-US/MSFT_ServiceResource.schema.mfl b/DscResources/MSFT_ServiceResource/en-US/MSFT_ServiceResource.schema.mfl index 46d774fd90ec1fe99c5e85a1a4e551b5b58c236f..ce9ea20ac6675c1c64fcfe9b4dacaa69da688f4e 100644 GIT binary patch delta 18 Zcmew*`bTtw1tX)vWJM0;%~6aCH~~LJ1;GFS delta 20 ccmew(`b%_!1>@vHj8c>BI5;-PFfQN(09F166aWAK diff --git a/Examples/Sample_Service_CreateService.ps1 b/Examples/Sample_Service_CreateService.ps1 index c0531f7..43c8698 100644 --- a/Examples/Sample_Service_CreateService.ps1 +++ b/Examples/Sample_Service_CreateService.ps1 @@ -1,60 +1,28 @@ +<# + .SYNOPSIS + If the service with the name Service1 does not exist, creates the service with the name + Service1 and the executable/binary path 'C:\FilePath\MyServiceExecutable.exe'. The new + service will be started by default. + + If the service with the name Service1 already exists, sets executable/binary path of the + service with the name Service1 to 'C:\FilePath\MyServiceExecutable.exe' and starts the + service by default if it is not running already. +#> Configuration Sample_Service_CreateService { - + [CmdletBinding()] param - ( - [System.String[]] - $nodeName = 'localhost', + () - [System.String] - $Name, - - [System.String] - [ValidateSet('Automatic', 'Manual', 'Disabled')] - $StartupType='Automatic', + Import-DscResource -ModuleName 'PSDscResources' - [System.String] - [ValidateSet('LocalSystem', 'LocalService', 'NetworkService')] - $BuiltInAccount='LocalSystem', - - [System.Management.Automation.PSCredential] - $Credential, - - [System.String] - [ValidateSet('Running', 'Stopped')] - $State='Running', - - [System.String] - [ValidateSet('Present', 'Absent')] - $Ensure='Present', - - [System.String] - $Path, - - [System.String] - $DisplayName, - - [System.String] - $Description, - - [System.String[]] - $Dependencies - ) - - Import-DscResource -Name MSFT_ServiceResource -ModuleName PSDscResources - - Node $nodeName + Node localhost { - Service service + Service ServiceResource1 { - Name = $Name - Ensure = $Ensure - Path = $Path + Name = 'Service1' + Ensure = 'Present' + Path = 'C:\FilePath\MyServiceExecutable.exe' } } } - - -Sample_Service_CreateService -Name 'Sample Service' -Ensure 'Present' -Path 'C:\DSC\TestService.exe' - - diff --git a/Examples/Sample_Service_DeleteService.ps1 b/Examples/Sample_Service_DeleteService.ps1 index 02c80d4..52c7a65 100644 --- a/Examples/Sample_Service_DeleteService.ps1 +++ b/Examples/Sample_Service_DeleteService.ps1 @@ -1,59 +1,21 @@ +<# + .SYNOPSIS + Stops and then removes the service with the name Service1. +#> Configuration Sample_Service_DeleteService { - + [CmdletBinding()] param - ( - [System.String[]] - $nodeName = 'localhost', + () - [System.String] - $Name, - - [System.String] - [ValidateSet('Automatic', 'Manual', 'Disabled')] - $StartupType, + Import-DscResource -ModuleName 'PSDscResources' - [System.String] - [ValidateSet('LocalSystem', 'LocalService', 'NetworkService')] - $BuiltInAccount, - - [System.Management.Automation.PSCredential] - $Credential , - - [System.String] - [ValidateSet('Running', 'Stopped')] - $State='Running', - - [System.String] - [ValidateSet('Present', 'Absent')] - $Ensure='Present', - - [System.String] - $Path, - - [System.String] - $DisplayName, - - [System.String] - $Description, - - [System.String[]] - $Dependencies - ) - - Import-DscResource -Name MSFT_ServiceResource -ModuleName PSDscResources - - Node $nodeName + Node localhost { - Service service + Service ServiceResource1 { - Name = $Name - Ensure = $Ensure + Name = 'Service1' + Ensure = 'Absent' } } } - - -Sample_Service_DeleteService -Name 'Sample Service' -Ensure 'Absent' - - diff --git a/Examples/Sample_Service_ServiceWithCredential.ps1 b/Examples/Sample_Service_ServiceWithCredential.ps1 deleted file mode 100644 index eb9114d..0000000 --- a/Examples/Sample_Service_ServiceWithCredential.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -Configuration Sample_Service_ServiceWithCredential -{ - - param - ( - [System.String[]] - $nodeName = 'localhost', - - [System.String] - $Name, - - [System.String] - [ValidateSet('Automatic', 'Manual', 'Disabled')] - $StartupType='Automatic', - - [System.String] - [ValidateSet('LocalSystem', 'LocalService', 'NetworkService')] - $BuiltInAccount='LocalSystem', - - [System.Management.Automation.PSCredential] - $Credential, - - [System.String] - [ValidateSet('Running', 'Stopped')] - $State='Running', - - [System.String] - [ValidateSet('Present', 'Absent')] - $Ensure='Present', - - [System.String] - $Path, - - [System.String] - $DisplayName, - - [System.String] - $Description, - - [System.String[]] - $Dependencies - ) - - Import-DscResource -Name MSFT_ServiceResource -ModuleName PSDscResources - - Node $nodeName - { - Service service - { - Name = $Name - DisplayName = $DisplayName - Ensure = $Ensure - Path = $Path - StartupType = $StartupType - Credential = $credential - DesktopInteract = 'False' - } - } -} - -# To use the sample(s) with credentials, see blog at http://blogs.msdn.com/b/powershell/archive/2014/01/31/want-to-secure-credentials-in-windows-powershell-desired-state-configuration.aspx - - - - - - - diff --git a/Examples/Sample_Service_UpdateStartupTypeIgnoreState.ps1 b/Examples/Sample_Service_UpdateStartupTypeIgnoreState.ps1 new file mode 100644 index 0000000..6db0411 --- /dev/null +++ b/Examples/Sample_Service_UpdateStartupTypeIgnoreState.ps1 @@ -0,0 +1,28 @@ +<# + .SYNOPSIS + If the service with the name Service1 does not exist, this configuration would throw an + error since the Path is not included here. + + If the service with the name Service1 already exists, sets the startup type of the service + with the name Service1 to Manual and ignores the state that the service is currently in. + If State is not specified, the configuration will ensure that the state of the service is + Running by default. +#> +Configuration Sample_Service_UpdateStartupTypeIgnoreState +{ + [CmdletBinding()] + param () + + Import-DscResource -ModuleName 'PSDscResources' + + Node localhost + { + Service ServiceResource1 + { + Name = 'Service1' + Ensure = 'Present' + StartupType = 'Manual' + State = 'Ignore' + } + } +} diff --git a/README.md b/README.md index d360b75..082533e 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,9 @@ Please check out the common DSC Resources [contributing guidelines](https://gith * [Group](#group): Provides a mechanism to manage local groups on a target node. * [Service](#service): Provides a mechanism to configure and manage Windows services on a target node. * [User](#user): Provides a mechanism to manage local users on a target node. -* [WindowsFeature](#windowsfeature): Provides a mechanism to install or uninstall windows roles or features on a target node. +* [WindowsFeature](#windowsfeature): Provides a mechanism to install or uninstall Windows roles or features on a target node. * [WindowsOptionalFeature](#windowsoptionalfeature): Provides a mechanism to enable or disable optional features on a target node. -* [WindowsPackageCab](#windowspackagecab): Provides a mechanism to install or uninstall a package from a windows cabinet (cab) file on a target node. +* [WindowsPackageCab](#windowspackagecab): Provides a mechanism to install or uninstall a package from a Windows cabinet (cab) file on a target node. * [WindowsProcess](#windowsprocess): Provides a mechanism to start and stop a Windows process. ### Resources that Work on Nano Server @@ -111,6 +111,7 @@ None * [Create a service](https://github.com/PowerShell/PSDscResources/blob/master/Examples/Sample_Service_CreateService.ps1) * [Delete a service](https://github.com/PowerShell/PSDscResources/blob/master/Examples/Sample_Service_DeleteService.ps1) +* [Update a service with StartupType set to 'Ignore'](https://github.com/PowerShell/PSDscResources/blob/master/Examples/Sample_Service_UpdateStartupTypeIgnoreState) ### User @@ -196,11 +197,11 @@ This resource works on Nano Server. #### Examples -* [Enable the specified windows optional feature and output logs to the specified path](https://github.com/PowerShell/PSDscResources/blob/master/Examples/Sample_WindowsOptionalFeature.ps1) +* [Enable the specified Windows optional feature and output logs to the specified path](https://github.com/PowerShell/PSDscResources/blob/master/Examples/Sample_WindowsOptionalFeature.ps1) ### WindowsPackageCab -Provides a mechanism to install or uninstall a package from a windows cabinet (cab) file on a target node. +Provides a mechanism to install or uninstall a package from a Windows cabinet (cab) file on a target node. This resource works on Nano Server. #### Requirements @@ -268,6 +269,8 @@ None * CommonTestHelper: * Added Get-AppVeyorAdministratorCredential * Added Set-StrictMode -'Latest' and $errorActionPreference -'Stop' +* Service: + * Updated resource module, tests, and examples to reflect the changes made in xPSDesiredStateConfiguration ### 2.1.0.0 diff --git a/Tests/Integration/MSFT_ServiceResource.Integration.Tests.ps1 b/Tests/Integration/MSFT_ServiceResource.Integration.Tests.ps1 index f1eef65..9ea8bc1 100644 --- a/Tests/Integration/MSFT_ServiceResource.Integration.Tests.ps1 +++ b/Tests/Integration/MSFT_ServiceResource.Integration.Tests.ps1 @@ -1,14 +1,25 @@ +<# + These tests should only be run in AppVeyor since they currently require the AppVeyor + administrator account credential to run. + + Also please note that these tests are currently dependent on each other. + They must be run in the order given and if one test fails, subsequent tests will + also fail. +#> + if ($PSVersionTable.PSVersion.Major -lt 5 -or $PSVersionTable.PSVersion.Minor -lt 1) { Write-Warning -Message 'Cannot run PSDscResources integration tests on PowerShell versions lower than 5.1' return } -$script:DscResourceName = 'MSFT_ServiceResource' +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' -Import-Module -Name (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) ` - -ChildPath (Join-Path -Path 'TestHelpers' ` - -ChildPath 'CommonTestHelper.psm1')) +# Import CommonTestHelper for Enter-DscResourceTestEnvironment, Exit-DscResourceTestEnvironment +$script:moduleRootPath = Split-Path -Path $PSScriptRoot -Parent +$script:testHelpersPath = Join-Path -Path $script:moduleRootPath -ChildPath 'TestHelpers' +Import-Module -Name (Join-Path -Path $script:testHelpersPath -ChildPath 'CommonTestHelper.psm1') $script:testEnvironment = Enter-DscResourceTestEnvironment ` -DscResourceModuleName 'PSDscResources' ` @@ -17,174 +28,386 @@ $script:testEnvironment = Enter-DscResourceTestEnvironment ` try { - $script:testServiceName = 'DscTestService' - $script:testServiceCodePath = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'TestHelpers\DscTestService.cs' - $script:testServiceDisplayName = 'DSC test service display name' - $script:testServiceDescription = 'This is a DSC test service used for integration testing MSFT_ServiceResource' - $script:testServiceDependsOn = 'winrm' - $script:testServiceExecutablePath = Join-Path -Path $ENV:Temp -ChildPath 'DscTestService.exe' - $script:testServiceNewCodePath = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'TestHelpers\DscTestService.cs' - $script:testServiceNewDisplayName = 'New: DSC test service display name' - $script:testServiceNewDescription = 'New: This is a DSC test service used for integration testing MSFT_ServiceResource' - $script:testServiceNewDependsOn = 'spooler' - $script:testServiceNewExecutablePath = Join-Path -Path $ENV:Temp -ChildPath 'NewDscTestService.exe' - - <# - Nano Server doesn't recognize 'spooler', so if these tests are being run on Nano - this value must stay as 'winrm' - #> - if ($PSVersionTable.PSEdition -ieq 'Core') { $script:testServiceNewDependsOn = 'winrm' } + Describe 'Service Integration Tests' { + BeforeAll { + # Import CommonResourceHelper for Test-IsNanoServer, Get-AppveyorAdministratorCredential + $moduleRootFilePath = Split-Path -Path $script:testsFolderFilePath -Parent + $dscResourcesFolderFilePath = Join-Path -Path $moduleRootFilePath -ChildPath 'DscResources' + $commonResourceHelperFilePath = Join-Path -Path $dscResourcesFolderFilePath -ChildPath 'CommonResourceHelper.psm1' + Import-Module -Name $commonResourceHelperFilePath - Import-Module -Name (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) ` - -ChildPath 'TestHelpers\MSFT_ServiceResource.TestHelper.psm1') ` - -Force + # Import DscResource.Tests TestHelper for Reset-Dsc + $dscResourceTestsFilePath = Join-Path -Path $moduleRootFilePath -ChildPath 'DscResource.Tests' + $dscResourceTestHelperFilePath = Join-Path -Path $dscResourceTestsFilePath -ChildPath 'TestHelper.psm1' + Import-Module -Name $dscResourceTestHelperFilePath - Stop-Service $script:testServiceName -ErrorAction SilentlyContinue + # Import Service test helper for New-ServiceBinary, Test-ServiceExists, Remove-ServiceWithTimeout + $serviceTestHelperFilePath = Join-Path -Path $script:testsHelpersFilePath -ChildPath 'MSFT_ServiceResource.TestHelper.psm1' + Import-Module -Name $serviceTestHelperFilePath - # Create new Service binaries for the new service. - New-ServiceBinary ` - -ServiceName $script:testServiceName ` - -ServiceCodePath $script:testServiceCodePath ` - -ServiceDisplayName $script:testServiceDisplayName ` - -ServiceDescription $script:testServiceDescription ` - -ServiceDependsOn $script:testServiceDependsOn ` - -ServiceExecutablePath $script:testServiceExecutablePath + # Import Service resource module for Test-TargetResource + $serviceResourceFolderFilePath = Join-Path -Path $dscResourcesFolderFilePath -ChildPath 'MSFT_ServiceResource' + $serviceResourceModuleFilePath = Join-Path -Path $serviceResourceFolderFilePath -ChildPath 'MSFT_ServiceResource.psm1' + Import-Module -Name $serviceResourceModuleFilePath - New-ServiceBinary ` - -ServiceName $script:testServiceName ` - -ServiceCodePath $script:testServiceNewCodePath ` - -ServiceDisplayName $script:testServiceNewDisplayName ` - -ServiceDescription $script:testServiceNewDescription ` - -ServiceDependsOn $script:testServiceNewDependsOn ` - -ServiceExecutablePath $script:testServiceExecutablePath + # Configuration file paths + $script:configurationAllExceptCredentialFilePath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_ServiceResource_AllExceptCredential.config.ps1' + $script:configurationCredentialOnlyFilePath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_ServiceResource_CredentialOnly.config.ps1' - #region Integration Tests - $configFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:DscResourceName)_Add.config.ps1" - . $configFile + # Create test service binary to be the existing service + $script:existingServiceProperties = @{ + Name = 'TestService' + DisplayName = 'TestDisplayName' + Description = 'Test service description' + Dependencies = @( 'winrm' ) + Path = Join-Path -Path $TestDrive -ChildPath 'DscTestService.exe' + } - Describe "$($script:DscResourceName) Add Service" { + $existingServiceNewExecutableParameters = @{ + ServiceName = $script:existingServiceProperties.Name + ServiceCodePath = Join-Path -Path $script:testsHelpersPath -ChildPath 'DscTestService.cs' + ServiceDisplayName = $script:existingServiceProperties.DisplayName + ServiceDescription = $script:existingServiceProperties.Description + ServiceDependsOn = $script:existingServiceProperties.Dependencies -join ', ' + OutputPath = $script:existingServiceProperties.Path + } - It 'Should compile and apply the MOF without throwing' { + New-ServiceExecutable @existingServiceNewExecutableParameters + + # Create test service binary to be the new service with the same name as the existing service + $script:newServiceProperties = @{ + Name = $script:existingServiceProperties.Name + DisplayName = 'NewTestDisplayName' + Description = 'New test service description' + Dependencies = @( 'spooler' ) + Path = Join-Path -Path $TestDrive -ChildPath 'NewDscTestService.exe' + } + + if (Test-IsNanoServer) { - & "$($script:DscResourceName)_Add_Config" ` - -OutputPath $script:testEnvironment.WorkingFolder ` - -ServiceName $script:testServiceName ` - -ServicePath $script:testServiceExecutablePath ` - -ServiceDisplayName $script:testServiceDisplayName ` - -ServiceDescription $script:testServiceDescription ` - -ServiceDependsOn $script:testServiceDependsOn + # Nano Server does not recognize 'spooler', so keep the dependencies value as 'winrm' + $newServiceProperties['Dependencies'] = @( 'winrm' ) + } - Start-DscConfiguration -Path $script:testEnvironment.WorkingFolder ` - -ComputerName localhost -Wait -Verbose -Force - } | Should Not Throw + $newServiceNewExecutableParameters = @{ + ServiceName = $script:newServiceProperties.Name + ServiceCodePath = Join-Path -Path $script:testHelpersPath -ChildPath 'DscTestServiceNew.cs' + ServiceDisplayName = $script:newServiceProperties.DisplayName + ServiceDescription = $script:newServiceProperties.Description + ServiceDependsOn = $script:newServiceProperties.Dependencies -join ', ' + OutputPath = $script:newServiceProperties.Path + } + + New-ServiceExecutable @newServiceNewExecutableParameters + + $script:testServiceNames = @( $script:existingServiceProperties.Name ) + $script:testServiceExecutables = @( $script:existingServiceProperties.Path, $script:newServiceProperties.Path ) } - It 'Should be able to call Get-DscConfiguration without throwing' { - { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not Throw + AfterAll { + # Remove any created services + foreach ($testServiceName in $script:testServiceNames) + { + if (Test-ServiceExists -Name $testServiceName) + { + Remove-ServiceWithTimeout -Name $testServiceName + } + } } - $script:service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$($script:testServiceName)'" - It 'Should return a service of type CimInstance' { - $script:service | Should BeOfType 'Microsoft.Management.Infrastructure.CimInstance' + Context 'Create a service' { + Reset-DSC + + $configurationName = 'TestCreateService' + $resourceParameters = $script:existingServiceProperties + + It 'Should compile and apply the MOF without throwing' { + { + . $script:configurationAllExceptCredentialFilePath -ConfigurationName $configurationName + & $configurationName -OutputPath $TestDrive @resourceParameters + Start-DscConfiguration -Path $TestDrive -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + $service = Get-Service -Name $resourceParameters.Name -ErrorAction 'SilentlyContinue' + $serviceCimInstance = Get-CimInstance -ClassName 'Win32_Service' -Filter "Name='$($resourceParameters.Name)'" -ErrorAction 'SilentlyContinue' + + It 'Should have created a new service with the specified name' { + $service | Should Not Be $null + $serviceCimInstance | Should Not Be $null + + $service.Name | Should Be $resourceParameters.Name + $serviceCimInstance.Name | Should Be $resourceParameters.Name + } + + It 'Should have created a new service with the specified path' { + $serviceCimInstance.PathName | Should Be $resourceParameters.Path + } + + It 'Should have created a new service with the specified display name' { + $service.DisplayName | Should Be $resourceParameters.DisplayName + } + + It 'Should have created a new service with the specified description' { + $serviceCimInstance.Description | Should Be $resourceParameters.Description + } + + It 'Should have created a new service with the specified dependencies' { + $differentDependencies = Compare-Object -ReferenceObject $resourceParameters.Dependencies -DifferenceObject $service.ServicesDependedOn.Name + $differentDependencies | Should Be $null + } + + It 'Should have created a new service with the default state as Running' { + $service.Status | Should Be 'Running' + } + + It 'Should have created a new service with the default startup type as Auto' { + $serviceCimInstance.StartMode | Should Be 'Auto' + } + + It 'Should have created a new service with the default startup account name as LocalSystem' { + $serviceCimInstance.StartName | Should Be 'LocalSystem' + } + + It 'Should have created a new service with the default desktop interaction setting as False' { + $serviceCimInstance.DesktopInteract | Should Be $false + } + + It 'Should return true from Test-TargetResource with the same parameters' { + MSFT_ServiceResource\Test-TargetResource @resourceParameters | Should Be $true + } } - It 'Should have set the resource and all the parameters should match' { - # Get the current service details - $script:service.status | Should Be 'OK' - $script:service.pathname | Should Be $script:testServiceExecutablePath - $script:service.description | Should Be $script:testServiceDescription - $script:service.displayname | Should Be $script:testServiceDisplayName - $script:service.started | Should Be $true - $script:service.state | Should Be 'Running' - $script:service.desktopinteract | Should Be $true - $script:service.startname | Should Be 'LocalSystem' - $script:service.startmode | Should Be 'Auto' + Context 'Edit the service path, display name, description, and dependencies' { + Reset-DSC + + $configurationName = 'TestCreateService' + $resourceParameters = $script:newServiceProperties + + It 'Should compile and apply the MOF without throwing' { + { + . $script:configurationAllExceptCredentialFilePath -ConfigurationName $configurationName + & $configurationName -OutputPath $TestDrive @resourceParameters + Start-DscConfiguration -Path $TestDrive -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + $service = Get-Service -Name $resourceParameters.Name -ErrorAction 'SilentlyContinue' + $serviceCimInstance = Get-CimInstance -ClassName 'Win32_Service' -Filter "Name='$($resourceParameters.Name)'" -ErrorAction 'SilentlyContinue' + + It 'Should not have removed service with specified name' { + $service | Should Not Be $null + $serviceCimInstance | Should Not Be $null + + $service.Name | Should Be $resourceParameters.Name + $serviceCimInstance.Name | Should Be $resourceParameters.Name + } + + It 'Should have edited service to have the specified path' { + $serviceCimInstance.PathName | Should Be $resourceParameters.Path + } + + It 'Should have edited service to have the specified display name' { + $service.DisplayName | Should Be $resourceParameters.DisplayName + } + + It 'Should have edited service to have the specified description' { + $serviceCimInstance.Description | Should Be $resourceParameters.Description + } + + It 'Should have edited service to have the specified dependencies' { + $differentDependencies = Compare-Object -ReferenceObject $resourceParameters.Dependencies -DifferenceObject $service.ServicesDependedOn.Name + $differentDependencies | Should Be $null + } + + It 'Should not have changed the service state from Running' { + $service.Status | Should Be 'Running' + } + + It 'Should not have changed the service startup type from Auto' { + $serviceCimInstance.StartMode | Should Be 'Auto' + } + + It 'Should not have changed the service startup account name from LocalSystem' { + $serviceCimInstance.StartName | Should Be 'LocalSystem' + } + + It 'Should not have changed the service desktop interaction setting from False' { + $serviceCimInstance.DesktopInteract | Should Be $false + } + + It 'Should return true from Test-TargetResource with the same parameters' { + MSFT_ServiceResource\Test-TargetResource @resourceParameters | Should Be $true + } + } + + Context 'Edit the service startup type and state' { + Reset-DSC + + $configurationName = 'TestCreateService' + $resourceParameters = @{ + Name = $script:existingServiceProperties.Name + Path = $script:newServiceProperties.Path + StartupType = 'Manual' + State = 'Stopped' + } + + It 'Should compile and apply the MOF without throwing' { + { + . $script:configurationAllExceptCredentialFilePath -ConfigurationName $configurationName + & $configurationName -OutputPath $TestDrive @resourceParameters + Start-DscConfiguration -Path $TestDrive -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + $service = Get-Service -Name $resourceParameters.Name -ErrorAction 'SilentlyContinue' + $serviceCimInstance = Get-CimInstance -ClassName 'Win32_Service' -Filter "Name='$($resourceParameters.Name)'" -ErrorAction 'SilentlyContinue' + + It 'Should not have removed service with specified name' { + $service | Should Not Be $null + $serviceCimInstance | Should Not Be $null + + $service.Name | Should Be $resourceParameters.Name + $serviceCimInstance.Name | Should Be $resourceParameters.Name + } + + It 'Should have edited the service to have the specified state' { + $service.Status | Should Be $resourceParameters.State + } + + It 'Should have edited the service to have the specified startup type' { + $serviceCimInstance.StartMode | Should Be $resourceParameters.StartupType + } + + It 'Should return true from Test-TargetResource with the same parameters' { + MSFT_ServiceResource\Test-TargetResource @resourceParameters | Should Be $true + } + } + + Context 'Edit the service start name and start password with Credential' { + Reset-DSC + + $configData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + PSDscAllowPlainTextPassword = $true + } + ) + } + + $configurationName = 'TestCreateService' + $resourceParameters = @{ + Name = 'TestService' + Credential = Get-AppVeyorAdministratorCredential + } + + It 'Should compile and apply the MOF without throwing' { + { + . $script:configurationCredentialOnlyFilePath -ConfigurationName $configurationName + & $configurationName -OutputPath $TestDrive -ConfigurationData $configData @resourceParameters + Start-DscConfiguration -Path $TestDrive -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + $service = Get-Service -Name $resourceParameters.Name -ErrorAction 'SilentlyContinue' + $serviceCimInstance = Get-CimInstance -ClassName 'Win32_Service' -Filter "Name='$($resourceParameters.Name)'" -ErrorAction 'SilentlyContinue' + + It 'Should not have removed service with specified name' { + $service | Should Not Be $null + $serviceCimInstance | Should Not Be $null + + $service.Name | Should Be $resourceParameters.Name + $serviceCimInstance.Name | Should Be $resourceParameters.Name + } + + It 'Should have edited the service to have the specified startup account name' { + $expectedStartName = $resourceParameters.Credential.UserName + + if ($expectedStartName.StartsWith("$env:COMPUTERNAME\")) + { + $expectedStartName = $expectedStartName.TrimStart("$env:COMPUTERNAME\") + } + + $serviceCimInstance.StartName | Should Be ".\$expectedStartName" + } + + It 'Should return true from Test-TargetResource with the same parameters' { + MSFT_ServiceResource\Test-TargetResource @resourceParameters | Should Be $true + } + } + + Context 'Edit the service start name and start password with BuiltInAccount' { + Reset-DSC + + $configurationName = 'TestCreateService' + $resourceParameters = @{ + Name = 'TestService' + Path = $script:newServiceProperties.Path + BuiltInAccount = 'LocalService' + } + + It 'Should compile and apply the MOF without throwing' { + { + . $script:configurationAllExceptCredentialFilePath -ConfigurationName $configurationName + & $configurationName -OutputPath $TestDrive @resourceParameters + Start-DscConfiguration -Path $TestDrive -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + $service = Get-Service -Name $resourceParameters.Name -ErrorAction 'SilentlyContinue' + $serviceCimInstance = Get-CimInstance -ClassName 'Win32_Service' -Filter "Name='$($resourceParameters.Name)'" -ErrorAction 'SilentlyContinue' + + It 'Should not have removed service with specified name' { + $service | Should Not Be $null + $serviceCimInstance | Should Not Be $null + + $service.Name | Should Be $resourceParameters.Name + $serviceCimInstance.Name | Should Be $resourceParameters.Name + } + + It 'Should have edited the service to have the specified startup account name' { + $serviceCimInstance.StartName | Should Be 'NT Authority\LocalService' + } + + It 'Should return true from Test-TargetResource with the same parameters' { + MSFT_ServiceResource\Test-TargetResource @resourceParameters | Should Be $true + } + } + + Context 'Remove the service' { + Reset-DSC + + $configurationName = 'TestCreateService' + $resourceParameters = @{ + Name = $script:existingServiceProperties.Name + Path = $script:existingServiceProperties.Path + Ensure = 'Absent' + } + + It 'Should compile and apply the MOF without throwing' { + { + . $script:configurationAllExceptCredentialFilePath -ConfigurationName $configurationName + & $configurationName -OutputPath $TestDrive @resourceParameters + Start-DscConfiguration -Path $TestDrive -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + $service = Get-Service -Name $resourceParameters.Name -ErrorAction 'SilentlyContinue' + $serviceCimInstance = Get-CimInstance -ClassName 'Win32_Service' -Filter "Name='$($resourceParameters.Name)'" -ErrorAction 'SilentlyContinue' + + It 'Should have removed the service with specified name' { + $service | Should Be $null + $serviceCimInstance | Should Be $null + } + + It 'Should return true from Test-TargetResource with the same parameters' { + MSFT_ServiceResource\Test-TargetResource @resourceParameters | Should Be $true + } } } - - Reset-DSC - - $configFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:DscResourceName)_Edit.config.ps1" - . $configFile - - Describe "$($script:DscResourceName) Edit Service" { - - It 'Should compile and apply the MOF without throwing' { - { - & "$($script:DscResourceName)_Edit_Config" ` - -OutputPath $script:testEnvironment.WorkingFolder ` - -ServiceName $script:testServiceName ` - -ServicePath $script:testServiceNewExecutablePath ` - -ServiceDisplayName $script:testServiceNewDisplayName ` - -ServiceDescription $script:testServiceNewDescription ` - -ServiceDependsOn $script:testServiceNewDependsOn - - Start-DscConfiguration -Path $script:testEnvironment.WorkingFolder ` - -ComputerName localhost -Wait -Verbose -Force - } | Should Not Throw - } - - It 'Should be able to call Get-DscConfiguration without throwing' { - { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not Throw - } - - # Get the current service details - $script:service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$($script:testServiceName)'" - It 'Should return a service or type CimInstance' { - $script:service | Should BeOfType 'Microsoft.Management.Infrastructure.CimInstance' - } - - It 'Should have set the resource and all the parameters should match' { - # Get the current service details - $script:service.status | Should Be 'OK' - $script:service.pathname | Should Be $script:testServiceNewExecutablePath - $script:service.description | Should Be $script:testServiceNewDescription - $script:service.displayname | Should Be $script:testServiceNewDisplayName - $script:service.started | Should Be $false - $script:service.state | Should Be 'Stopped' - $script:service.desktopinteract | Should Be $false - $script:service.startname | Should Be 'NT Authority\LocalService' - $script:service.startmode | Should Be 'Manual' - } - } - - Reset-DSC - - $configFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:DscResourceName)_Remove.config.ps1" - . $configFile - - Describe "$($script:DscResourceName) Remove Service" { - - It 'Should compile and apply the MOF without throwing' { - { - & "$($script:DscResourceName)_Remove_Config" ` - -OutputPath $script:testEnvironment.WorkingFolder ` - -ServiceName $script:testServiceName - - Start-DscConfiguration -Path $script:testEnvironment.WorkingFolder ` - -ComputerName localhost -Wait -Verbose -Force - } | Should not throw - } - - It 'Should be able to call Get-DscConfiguration without throwing' { - { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not throw - } - - # Get the current service details - $script:service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$($script:testServiceName)'" - - It 'Should return the service as null' { - $script:service | Should BeNullOrEmpty - } - } - #endregion } finally { - # Clean up - Remove-TestService ` - -ServiceName $script:testServiceName ` - -ServiceExecutablePath $script:testServiceExecutablePath - Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment } diff --git a/Tests/Integration/MSFT_ServiceResource_AllExceptCredential.config.ps1 b/Tests/Integration/MSFT_ServiceResource_AllExceptCredential.config.ps1 new file mode 100644 index 0000000..8311b7e --- /dev/null +++ b/Tests/Integration/MSFT_ServiceResource_AllExceptCredential.config.ps1 @@ -0,0 +1,89 @@ +param +( + [Parameter(Mandatory = $true)] + [String] + $ConfigurationName +) + +Configuration $ConfigurationName +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path, + + [ValidateSet('Present', 'Absent')] + [ValidateNotNullOrEmpty()] + [String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $DisplayName = "$Name DisplayName", + + [Parameter()] + [String] + $Description = 'TestDescription', + + [Parameter()] + [String[]] + [AllowEmptyCollection()] + $Dependencies = @(), + + [Parameter()] + [ValidateSet('LocalSystem', 'LocalService', 'NetworkService')] + [String] + $BuiltInAccount = 'LocalSystem', + + [Parameter()] + [Boolean] + $DesktopInteract = $false, + + [Parameter()] + [ValidateSet('Automatic', 'Manual', 'Disabled')] + [String] + $StartupType = 'Automatic', + + [Parameter()] + [ValidateSet('Running', 'Stopped', 'Ignore')] + [String] + $State = 'Running', + + [Parameter()] + [UInt32] + $StartupTimeout = 30000, + + [Parameter()] + [UInt32] + $TerminateTimeout = 30000 + ) + + Import-DscResource -ModuleName 'PSDscResources' + + Node localhost + { + Service Service1 + { + Name = $Name + Ensure = $Ensure + Path = $Path + StartupType = $StartupType + BuiltInAccount = $BuiltInAccount + DesktopInteract = $DesktopInteract + State = $State + DisplayName = $DisplayName + Description = $Description + Dependencies = $Dependencies + StartupTimeout = $StartupTimeout + TerminateTimeout = $TerminateTimeout + } + } +} diff --git a/Tests/Integration/MSFT_ServiceResource_CredentialOnly.config.ps1 b/Tests/Integration/MSFT_ServiceResource_CredentialOnly.config.ps1 new file mode 100644 index 0000000..4a49e23 --- /dev/null +++ b/Tests/Integration/MSFT_ServiceResource_CredentialOnly.config.ps1 @@ -0,0 +1,39 @@ +param +( + [Parameter(Mandatory = $true)] + [String] + $ConfigurationName +) + +Configuration $ConfigurationName +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Name, + + [ValidateSet('Present', 'Absent')] + [ValidateNotNullOrEmpty()] + [String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -ModuleName 'PSDscResources' + + Node localhost + { + Service Service1 + { + Name = $Name + Ensure = $Ensure + Credential = $Credential + } + } +} diff --git a/Tests/Integration/MSFT_ServiceResource_add.config.ps1 b/Tests/Integration/MSFT_ServiceResource_add.config.ps1 deleted file mode 100644 index dd0da30..0000000 --- a/Tests/Integration/MSFT_ServiceResource_add.config.ps1 +++ /dev/null @@ -1,27 +0,0 @@ -Configuration MSFT_ServiceResource_Add_Config { - param - ( - $ServiceName, - $ServicePath, - $ServiceDisplayName, - $ServiceDescription, - $ServiceDependsOn - ) - - Import-DscResource -ModuleName PSDscResources - - node localhost { - Service AddService { - Name = $ServiceName - Ensure = 'Present' - Path = $ServicePath - StartupType = 'Automatic' - BuiltInAccount = 'LocalSystem' - DesktopInteract = $true - State = 'Running' - DisplayName = $ServiceDisplayName - Description = $ServiceDescription - Dependencies = $ServiceDependsOn - } - } -} diff --git a/Tests/Integration/MSFT_ServiceResource_edit.config.ps1 b/Tests/Integration/MSFT_ServiceResource_edit.config.ps1 deleted file mode 100644 index 399db6d..0000000 --- a/Tests/Integration/MSFT_ServiceResource_edit.config.ps1 +++ /dev/null @@ -1,27 +0,0 @@ -Configuration MSFT_ServiceResource_Edit_Config { - param - ( - $ServiceName, - $ServicePath, - $ServiceDisplayName, - $ServiceDescription, - $ServiceDependsOn - ) - - Import-DscResource -ModuleName PSDscResources - - node localhost { - Service EditService { - Name = $ServiceName - Ensure = 'Present' - Path = $ServicePath - StartupType = 'Manual' - BuiltInAccount = 'LocalService' - DesktopInteract = $false - State = 'Stopped' - DisplayName = $ServiceDisplayName - Description = $ServiceDescription - Dependencies = $ServiceDependsOn - } - } -} diff --git a/Tests/Integration/MSFT_ServiceResource_remove.config.ps1 b/Tests/Integration/MSFT_ServiceResource_remove.config.ps1 deleted file mode 100644 index 3ff1e7a..0000000 --- a/Tests/Integration/MSFT_ServiceResource_remove.config.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -Configuration MSFT_ServiceResource_Remove_Config { - param - ( - $ServiceName - ) - - Import-DscResource -ModuleName PSDscResources - - node localhost { - Service RemoveService { - Name = $ServiceName - Ensure = 'Absent' - } - } -} diff --git a/Tests/TestHelpers/CommonTestHelper.psm1 b/Tests/TestHelpers/CommonTestHelper.psm1 index 4177e30..4ba611b 100644 --- a/Tests/TestHelpers/CommonTestHelper.psm1 +++ b/Tests/TestHelpers/CommonTestHelper.psm1 @@ -1,4 +1,4 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] param () $errorActionPreference = 'Stop' @@ -87,6 +87,7 @@ function Test-IsLocalMachine NOTE: This is likely overkill; consider removing it. #> $networkAdapters = @(Get-CimInstance Win32_NetworkAdapterConfiguration) + foreach ($networkAdapter in $networkAdapters) { if ($null -ne $networkAdapter.IPAddress) diff --git a/Tests/TestHelpers/MSFT_ServiceResource.TestHelper.psm1 b/Tests/TestHelpers/MSFT_ServiceResource.TestHelper.psm1 index 8d5d30f..37c6d42 100644 --- a/Tests/TestHelpers/MSFT_ServiceResource.TestHelper.psm1 +++ b/Tests/TestHelpers/MSFT_ServiceResource.TestHelper.psm1 @@ -1,153 +1,140 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + <# .SYNOPSIS - Creates a service binary file. + Creates a service executable. .PARAMETER ServiceName - The name of the service to create the binary file for. + The name of the service to create the executable for. .PARAMETER ServiceCodePath - The path to the code for the service to create the binary file for. + The path to the code for the service to create the executable for. .PARAMETER ServiceDisplayName - The display name of the service to create the binary file for. + The display name of the service to create the executable for. .PARAMETER ServiceDescription - The description of the service to create the binary file for. + The description of the service to create the executable for. .PARAMETER ServiceDependsOn - Dependencies of the service to create the binary file for. + The names of the dependencies of the service to create the executable for. - .PARAMETER ServiceExecutablePath - The path to write the service executable to. + .PARAMETER OutputPath + The path to write the outputed service executable to. #> -function New-ServiceBinary +function New-ServiceExecutable { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [String] $ServiceName, [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [String] $ServiceCodePath, [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [String] $ServiceDisplayName, [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [String] $ServiceDescription, - [Parameter(Mandatory = $true)] + [Parameter()] + [ValidateNotNullOrEmpty()] [String] - $ServiceDependsOn, + $ServiceDependsOn = "''", [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [String] - $ServiceExecutablePath + $OutputPath ) - if (Get-Service $ServiceName -ErrorAction Ignore) - { - Stop-Service $ServiceName -ErrorAction SilentlyContinue - Remove-TestService -ServiceName $ServiceName -ServiceExecutablePath $ServiceExecutablePath - } - - $fileText = Get-Content $ServiceCodePath -Raw + $fileText = Get-Content -Path $ServiceCodePath -Raw $fileText = $fileText.Replace('TestServiceReplacementName', $ServiceName) $fileText = $fileText.Replace('TestServiceReplacementDisplayName', $ServiceDisplayName) $fileText = $fileText.Replace('TestServiceReplacementDescription', $ServiceDescription) $fileText = $fileText.Replace('TestServiceReplacementDependsOn', $ServiceDependsOn) - Add-Type $fileText ` - -OutputAssembly $ServiceExecutablePath ` - -OutputType WindowsApplication ` - -ReferencedAssemblies @('System.ServiceProcess', 'System.Configuration.Install') + + $addTypeParameters = @{ + TypeDefinition = $fileText + OutputAssembly = $OutputPath + OutputType = 'WindowsApplication' + ReferencedAssemblies = @( 'System.ServiceProcess', 'System.Configuration.Install' ) + } + + $null = Add-Type @addTypeParameters } <# .SYNOPSIS - Retrieves the path to the install utility. + Deletes the service with the given name and waits 5 seconds maximum for the service to be + deleted. + + .PARAMETER Name + The name of the service to delete. #> -function Get-InstallUtilPath -{ - [CmdletBinding()] - param () - - if ($env:Processor_Architecture -ieq 'amd64') - { - $frameworkName = 'Framework64' - } - else - { - $frameworkName = 'Framework' - } - - return Join-Path -Path (Resolve-Path "$env:WinDir\Microsoft.Net\$frameworkName\v4*") ` - -ChildPath 'installUtil.exe' -} - -<# - .SYNOPSIS - Removes a service. - - .PARAMETER ServiceName - The name of the service to remove. - - .PARAMETER ServiceExecutablePath - The path to the executable of the service to remove. -#> -function Remove-TestService -{ - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [String] - $ServiceName, - - [Parameter(Mandatory = $true)] - [String] - $ServiceExecutablePath - ) - - if (Get-Service -Name $ServiceName -ErrorAction SilentlyContinue) - { - $installUtility = Get-InstallUtilPath - & $installUtility /u $ServiceExecutablePath - } - - Remove-Item $ServiceExecutablePath -Force -ErrorAction SilentlyContinue - Remove-Item *.InstallLog -Force -ErrorAction SilentlyContinue - Remove-Item $ServiceName -Force -Recurse -ErrorAction SilentlyContinue -} - -function Get-InvalidArgumentRecord +function Remove-ServiceWithTimeout { [CmdletBinding()] param ( - [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [System.String] - $ErrorId, - - [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [System.String] - $ErrorMessage + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] + [String] + $Name ) - $exception = New-Object -TypeName System.InvalidOperationException ` - -ArgumentList $ErrorMessage - $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation - $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord ` - -ArgumentList $exception, $ErrorId, $errorCategory, $null - return $errorRecord + Stop-Service -Name $Name + + & 'sc.exe' 'delete' $Name + + $serviceDeleted = $false + $start = [DateTime]::Now + + while (-not $serviceDeleted -and ([DateTime]::Now - $start).TotalMilliseconds -lt 5000) + { + $service = Get-Service -Name $Name -ErrorAction 'SilentlyContinue' + + if ($null -eq $service) + { + $serviceDeleted = $true + } + else + { + Start-Sleep -Seconds 1 + } + } } -Export-ModuleMember -Function ` - New-ServiceBinary, ` - New-TestService, ` - Remove-TestService, ` - Get-InvalidArgumentRecord +<# + .SYNOPSIS + Tests if the service with the specified name exists. + + .PARAMETER Name + The name of the service. +#> +function Test-ServiceExists +{ + [OutputType([Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name + ) + + $service = Get-Service -Name $Name -ErrorAction 'SilentlyContinue' + return $null -ne $service +} + +Export-ModuleMember -Function @( 'New-ServiceExecutable', 'Remove-ServiceWithTimeout', 'Test-ServiceExists' ) diff --git a/Tests/Unit/MSFT_ServiceResource.Tests.ps1 b/Tests/Unit/MSFT_ServiceResource.Tests.ps1 index 93809af..78c7a5c 100644 --- a/Tests/Unit/MSFT_ServiceResource.Tests.ps1 +++ b/Tests/Unit/MSFT_ServiceResource.Tests.ps1 @@ -1,2039 +1,2573 @@ -# Need to be able to create a password from plain text for testing purposes +# Need to be able to create a password from plain text to create test credentials [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] param () -Import-Module -Name (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) ` - -ChildPath (Join-Path -Path 'TestHelpers' ` - -ChildPath 'CommonTestHelper.psm1')) +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +# Import CommonTestHelper for Enter-DscResourceTestEnvironment, Exit-DscResourceTestEnvironment +$script:moduleRootPath = Split-Path -Path $PSScriptRoot -Parent +$script:testHelpersPath = Join-Path -Path $script:moduleRootPath -ChildPath 'TestHelpers' +Import-Module -Name (Join-Path -Path $script:testHelpersPath -ChildPath 'CommonTestHelper.psm1') $script:testEnvironment = Enter-DscResourceTestEnvironment ` -DSCResourceModuleName 'PSDscResources' ` -DSCResourceName 'MSFT_ServiceResource' ` - -TestType Unit + -TestType 'Unit' -# Begin Testing try { - # This is needed so that the ServiceControllerStatus enum is recognized as a valid type Add-Type -AssemblyName 'System.ServiceProcess' InModuleScope 'MSFT_ServiceResource' { - - Import-Module -Name (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) ` - -ChildPath 'TestHelpers\MSFT_ServiceResource.TestHelper.psm1') ` - -Force - - $script:DscResourceName = 'MSFT_ServiceResource' - $script:testServiceName = 'DscTestService' - $script:testServiceDisplayName = 'DSC test service display name' - $script:testServiceDescription = 'This is the DSC test service used for unit testing MSFT_ServiceResource' - $script:testServiceDependsOn = @('winrm','spooler') - $script:testServiceDependsOnHash = @( @{ name = 'winrm' }, @{ name = 'spooler' } ) - $script:testServiceExecutablePath = Join-Path -Path $ENV:Temp -ChildPath 'DscTestService.exe' - $script:testServiceStartupType = 'Automatic' - $script:testServiceStartupTypeWin32 = 'Auto' - $script:testServiceStatusRunning = [System.ServiceProcess.ServiceControllerStatus]::Running - $script:testServiceStatusStopped = [System.ServiceProcess.ServiceControllerStatus]::Stopped - $script:testUsername = 'TestUser' + + $script:testUsername1 = 'TestUser1' + $script:testUsername2 = 'TestUser2' + $script:testPassword = 'DummyPassword' - $script:testCredential = New-Object ` - -TypeName System.Management.Automation.PSCredential ` - -ArgumentList ($script:testUsername, ` - (ConvertTo-SecureString $script:testPassword -AsPlainText -Force)) - $script:testNewUsername = 'DifferentUser' - $script:testNewCredential = New-Object ` - -TypeName System.Management.Automation.PSCredential ` - -ArgumentList ($script:testNewUsername, ` - (ConvertTo-SecureString $script:testPassword -AsPlainText -Force)) + $secureTestPassword = ConvertTo-SecureString $script:testPassword -AsPlainText -Force - $script:testServiceMockRunning = New-Object -TypeName PSObject -Property @{ - Name = $script:testServiceName - ServiceName = $script:testServiceName - DisplayName = $script:testServiceDisplayName - StartType = $script:testServiceStartupType - Status = $script:testServiceStatusRunning - ServicesDependedOn = $script:testServiceDependsOnHash - } + $script:testCredential1 = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList ($script:testUsername1, $secureTestPassword) + $script:testCredential2 = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList ($script:testUsername2, $secureTestPassword) - Add-Member -InputObject $script:testServiceMockRunning ` - -MemberType ScriptMethod ` - -Name Stop -Value { $global:ServiceStopped = $true } + Describe 'Service\Get-TargetResource' { + Mock -CommandName 'Get-Service' -MockWith { } + Mock -CommandName 'Get-ServiceCimInstance' -MockWith { } + Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { } - Add-Member -InputObject $script:testServiceMockRunning ` - -MemberType ScriptMethod ` - -Name WaitForStatus -Value { param( $Status, $WaitTimeSpan ) } - - $script:testServiceMockStopped = New-Object -TypeName PSObject -Property @{ - Name = $script:testServiceName - ServiceName = $script:testServiceName - DisplayName = $script:testServiceDisplayName - StartType = $script:testServiceStartupType - Status = $script:testServiceStatusStopped - ServicesDependedOn = $script:testServiceDependsOnHash - } - - Add-Member -InputObject $script:testServiceMockStopped ` - -MemberType ScriptMethod ` - -Name Start -Value { $global:ServiceStarted = $true } - - Add-Member -InputObject $script:testServiceMockStopped ` - -MemberType ScriptMethod ` - -Name WaitForStatus -Value { param( $Status, $WaitTimeSpan ) } - - $script:testWin32ServiceMockRunningLocalSystem = New-Object -TypeName PSObject -Property @{ - Name = $script:testServiceName - Status = 'OK' - DesktopInteract = $true - PathName = $script:testServiceExecutablePath - StartMode = $script:testServiceStartupTypeWin32 - Description = $script:testServiceDescription - Started = $true - DisplayName = $script:testServiceDisplayName - StartName = 'LocalSystem' - State = $script:testServiceStatusRunning - } - - $script:splatServiceExistsAutomatic = @{ - Name = $script:testServiceName - StartupType = $script:testServiceStartupType - BuiltInAccount = 'LocalSystem' - DesktopInteract = $true - State = $script:testServiceStatusRunning - Ensure = 'Present' - Path = $script:testServiceExecutablePath - DisplayName = $script:testServiceDisplayName - Description = $script:testServiceDescription - } - - Describe "$script:DscResourceName\Get-TargetResource" { - Context 'Service exists' { - # Mocks that should be called - Mock -CommandName Test-ServiceExists ` - -MockWith { $true } ` - -Verifiable - - Mock -CommandName Get-ServiceResource ` - -MockWith { $script:testServiceMockRunning } ` - -Verifiable - - Mock -CommandName Get-Win32ServiceObject ` - -MockWith { $script:testWin32ServiceMockRunningLocalSystem } ` - -Verifiable - - It 'Should not throw an exception' { - { $script:service = Get-TargetResource ` - -Name $script:testServiceName ` - } | Should Not Throw - } - - It 'Should return the correct hashtable properties' { - $service.Ensure | Should Be 'Present' - $service.Name | Should Be $script:testServiceName - $service.StartupType | Should Be $script:testServiceStartupType - $service.BuiltInAccount | Should Be 'LocalSystem' - $service.State | Should Be $script:testServiceStatusRunning - $service.Path | Should Be $script:testServiceExecutablePath - $service.DisplayName | Should Be $script:testServiceDisplayName - $service.Description | Should Be $script:testServiceDescription - $service.DesktopInteract | Should Be $true - $service.Dependencies | Should Be $script:testServiceDependsOn - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Get-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - } + $getTargetResourceParameters = @{ + Name = 'TestServiceName' } Context 'Service does not exist' { - # Mocks that should be called - Mock -CommandName Test-ServiceExists -MockWith { $false } -Verifiable - - # Mocks that should not be called - Mock -CommandName Get-serviceResource - - Mock -CommandName Get-Win32ServiceObject - - It 'Should not throw an exception' { - { $script:service = Get-TargetResource ` - -Name $script:testServiceName ` - } | Should Not Throw + It 'Should not throw' { + { $null = Get-TargetResource @getTargetResourceParameters } | Should Not Throw } - It 'Should return the correct hashtable properties' { - $service.Ensure | Should Be 'Absent' - $service.Name | Should Be $script:testServiceName + It 'Should retrieve service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $getTargetResourceParameters.Name } -Times 1 -Scope 'Context' } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Get-serviceResource -Exactly 0 - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 0 + It 'Should not attempt to retrieve the service CIM instance' { + Assert-MockCalled 'Get-ServiceCimInstance' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to convert the service start mode to a startup type string' { + Assert-MockCalled -CommandName 'ConvertTo-StartupTypeString' -Times 0 -Scope 'Context' + } + + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + + It 'Should return a hashtable' { + $getTargetResourceResult -is [Hashtable] | Should Be $true + } + + It 'Should return the service name' { + $getTargetResourceResult.Name | Should Be $getTargetResourceParameters.Name + } + + It 'Should return the service Ensure state as Absent' { + $getTargetResourceResult.Ensure | Should Be 'Absent' + } + } + + Context 'Service exists with all properties defined and custom startup account name' { + $testService = @{ + Name = 'TestServiceName' + DisplayName = 'TestDisplayName' + Status = 'TestServiceStatus' + StartType = 'TestServiceStartType' + ServicesDependedOn = @( + @{ + Name = 'ServiceDependency1' + }, + @{ + Name = 'ServiceDependency2' + } + ) + } + + $testServiceCimInstance = @{ + Name = $testService.Name + PathName = 'TestServicePath' + Description = 'Test service description' + StartName = 'CustomStartName' + StartMode = 'Auto' + DesktopInteract = $true + } + + $convertToStartupTypeStringResult = 'TestStartupTypeString' + + Mock -CommandName 'Get-Service' -MockWith { return $testService } + Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } + Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $convertToStartupTypeStringResult } + + It 'Should not throw' { + { $null = Get-TargetResource @getTargetResourceParameters } | Should Not Throw + } + + It 'Should retrieve service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $getTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should retrieve the service CIM instance' { + Assert-MockCalled 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $getTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should convert the service start mode to a startup type string' { + Assert-MockCalled -CommandName 'ConvertTo-StartupTypeString' -ParameterFilter { $StartMode -eq $testServiceCimInstance.StartMode } -Times 1 -Scope 'Context' + } + + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + + It 'Should return a hashtable' { + $getTargetResourceResult -is [Hashtable] | Should Be $true + } + + It 'Should return the service name' { + $getTargetResourceResult.Name | Should Be $getTargetResourceParameters.Name + } + + It 'Should return the service Ensure state as Present' { + $getTargetResourceResult.Ensure | Should Be 'Present' + } + + It 'Should return the service path' { + $getTargetResourceResult.Path | Should Be $testServiceCimInstance.PathName + } + + It 'Should return the service startup type' { + $getTargetResourceResult.StartupType | Should Be $convertToStartupTypeStringResult + } + + It 'Should return the service startup account name' { + $getTargetResourceResult.BuiltInAccount | Should Be $testServiceCimInstance.StartName + } + + It 'Should return the service state' { + $getTargetResourceResult.State | Should Be $testService.Status + } + + It 'Should return the service display name' { + $getTargetResourceResult.DisplayName | Should Be $testService.DisplayName + } + + It 'Should return the service description' { + $getTargetResourceResult.Description | Should Be $testServiceCimInstance.Description + } + + It 'Should return the service desktop interation setting' { + $getTargetResourceResult.DesktopInteract | Should Be $testServiceCimInstance.DesktopInteract + } + + It 'Should return the service dependencies' { + $getTargetResourceResult.Dependencies | Should Be $testService.ServicesDependedOn.Name + } + } + + Context 'Service exists with no dependencies and startup account name as NT Authority\LocalService' { + $testService = @{ + Name = 'TestServiceName' + DisplayName = 'TestDisplayName' + Status = 'TestServiceStatus' + StartType = 'TestServiceStartType' + ServicesDependedOn = $null + } + + $expectedBuiltInAccountValue = 'LocalService' + + $testServiceCimInstance = @{ + Name = $testService.Name + PathName = 'TestServicePath' + Description = 'Test service description' + StartName = "NT Authority\$expectedBuiltInAccountValue" + StartMode = 'Manual' + DesktopInteract = $false + } + + $convertToStartupTypeStringResult = 'TestStartupTypeString' + + Mock -CommandName 'Get-Service' -MockWith { return $testService } + Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } + Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $convertToStartupTypeStringResult } + + It 'Should not throw' { + { $null = Get-TargetResource @getTargetResourceParameters } | Should Not Throw + } + + It 'Should retrieve service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $getTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should retrieve the service CIM instance' { + Assert-MockCalled 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $getTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should convert the service start mode to a startup type string' { + Assert-MockCalled -CommandName 'ConvertTo-StartupTypeString' -ParameterFilter { $StartMode -eq $testServiceCimInstance.StartMode } -Times 1 -Scope 'Context' + } + + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + + It 'Should return a hashtable' { + $getTargetResourceResult -is [Hashtable] | Should Be $true + } + + It 'Should return the service name' { + $getTargetResourceResult.Name | Should Be $getTargetResourceParameters.Name + } + + It 'Should return the service Ensure state as Present' { + $getTargetResourceResult.Ensure | Should Be 'Present' + } + + It 'Should return the service path' { + $getTargetResourceResult.Path | Should Be $testServiceCimInstance.PathName + } + + It 'Should return the service startup type' { + $getTargetResourceResult.StartupType | Should Be $convertToStartupTypeStringResult + } + + It 'Should return the service startup account name' { + $getTargetResourceResult.BuiltInAccount | Should Be $expectedBuiltInAccountValue + } + + It 'Should return the service state' { + $getTargetResourceResult.State | Should Be $testService.Status + } + + It 'Should return the service display name' { + $getTargetResourceResult.DisplayName | Should Be $testService.DisplayName + } + + It 'Should return the service description' { + $getTargetResourceResult.Description | Should Be $testServiceCimInstance.Description + } + + It 'Should return the service desktop interation setting' { + $getTargetResourceResult.DesktopInteract | Should Be $testServiceCimInstance.DesktopInteract + } + + It 'Should return the service dependencies as null' { + $getTargetResourceResult.Dependencies | Should Be $null + } + } + + Context 'Service exists with no description or display name and startup account name as NT Authority\NetworkService' { + $testService = @{ + Name = 'TestServiceName' + DisplayName = $null + Status = 'TestServiceStatus' + StartType = 'TestServiceStartType' + ServicesDependedOn = @( + @{ + Name = 'ServiceDependency1' + }, + @{ + Name = 'ServiceDependency2' + } + ) + } + + $expectedBuiltInAccountValue = 'NetworkService' + + $testServiceCimInstance = @{ + Name = $testService.Name + PathName = 'TestServicePath' + Description = $null + StartName = "NT Authority\$expectedBuiltInAccountValue" + StartMode = 'Disabled' + DesktopInteract = $false + } + + $convertToStartupTypeStringResult = 'TestStartupTypeString' + + Mock -CommandName 'Get-Service' -MockWith { return $testService } + Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } + Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $convertToStartupTypeStringResult } + + It 'Should not throw' { + { $null = Get-TargetResource @getTargetResourceParameters } | Should Not Throw + } + + It 'Should retrieve service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $getTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should retrieve the service CIM instance' { + Assert-MockCalled 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $getTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should convert the service start mode to a startup type string' { + Assert-MockCalled -CommandName 'ConvertTo-StartupTypeString' -ParameterFilter { $StartMode -eq $testServiceCimInstance.StartMode } -Times 1 -Scope 'Context' + } + + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + + It 'Should return a hashtable' { + $getTargetResourceResult -is [Hashtable] | Should Be $true + } + + It 'Should return the service name' { + $getTargetResourceResult.Name | Should Be $getTargetResourceParameters.Name + } + + It 'Should return the service Ensure state as Present' { + $getTargetResourceResult.Ensure | Should Be 'Present' + } + + It 'Should return the service path' { + $getTargetResourceResult.Path | Should Be $testServiceCimInstance.PathName + } + + It 'Should return the service startup type' { + $getTargetResourceResult.StartupType | Should Be $convertToStartupTypeStringResult + } + + It 'Should return the service startup account name' { + $getTargetResourceResult.BuiltInAccount | Should Be $expectedBuiltInAccountValue + } + + It 'Should return the service state' { + $getTargetResourceResult.State | Should Be $testService.Status + } + + It 'Should return the service display name as null' { + $getTargetResourceResult.DisplayName | Should Be $null + } + + It 'Should return the service description as null' { + $getTargetResourceResult.Description | Should Be $null + } + + It 'Should return the service desktop interation setting' { + $getTargetResourceResult.DesktopInteract | Should Be $testServiceCimInstance.DesktopInteract + } + + It 'Should return the service dependencies' { + $getTargetResourceResult.Dependencies | Should Be $testService.ServicesDependedOn.Name } } } - Describe "$script:DscResourceName\Set-TargetResource" { - Context 'Service exists and should not' { - # Mocks that should be called - Mock -CommandName Test-StartupType ` - -Verifiable + Describe 'Service\Set-TargetResource' { + Mock -CommandName 'Assert-NoStartupTypeStateConflict' -MockWith { } - Mock -CommandName Test-ServiceExists ` - -MockWith { $true } ` - -Verifiable + Mock -CommandName 'Get-Service' -MockWith { } + Mock -CommandName 'New-Service' -MockWith { } + Mock -CommandName 'Remove-ServiceWithTimeout' -MockWith { } - Mock -CommandName Stop-ServiceResource ` - -Verifiable + Mock -CommandName 'Set-ServicePath' -MockWith { return $true } + Mock -CommandName 'Set-ServiceProperty' -MockWith { } - Mock -CommandName Remove-Service ` - -Verifiable + Mock -CommandName 'Start-ServiceWithTimeout' -MockWith { } + Mock -CommandName 'Stop-ServiceWithTimeout' -MockWith { } - # Mocks that should not be called - Mock -CommandName Start-ServiceResource - Mock -CommandName New-Service - Mock -CommandName Compare-ServicePath - Mock -CommandName Write-WriteProperty - - It 'Should not throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - $Splat.Ensure = 'Absent' - { Set-TargetResource @Splat } | Should Not Throw + Context 'Both BuiltInAccount and Credential specified' { + $setTargetResourceParameters = @{ + Name = $script:testServiceName + BuiltInAccount = 'LocalSystem' + Credential = $script:testCredential1 } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Start-ServiceResource -Exactly 0 - Assert-MockCalled -CommandName Stop-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName Remove-Service -Exactly 1 - Assert-MockCalled -CommandName New-Service -Exactly 0 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 0 - Assert-MockCalled -CommandName Write-WriteProperty -Exactly 0 + It 'Should throw an error for BuiltInAccount and Credential conflict' { + $expectedErrorMessage = $script:localizedData.BuiltInAccountAndCredentialSpecified -f $setTargetResourceParameters.Name + { Set-TargetResource @setTargetResourceParameters } | Should Throw $expectedErrorMessage } } - Context 'Service exists and should, should be running, all parameters passed' { - # Mocks that should be called - Mock -CommandName Test-StartupType ` - -Verifiable - - Mock -CommandName Test-ServiceExists ` - -MockWith { $true } ` - -Verifiable - - Mock -CommandName Compare-ServicePath ` - -MockWith { $true } ` - -Verifiable - - Mock -CommandName Write-WriteProperty ` - -MockWith { $false } ` - -Verifiable - - Mock -CommandName Start-ServiceResource ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName Stop-ServiceResource - Mock -CommandName New-Service - Mock -CommandName Remove-Service - - It 'Should not throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - { Set-TargetResource @Splat } | Should Not Throw + Context 'Service does not exist and Ensure set to Absent' { + $setTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Absent' } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Start-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName Stop-ServiceResource -Exactly 0 - Assert-MockCalled -CommandName Remove-Service -Exactly 0 - Assert-MockCalled -CommandName New-Service -Exactly 0 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 1 - Assert-MockCalled -CommandName Write-WriteProperty -Exactly 1 + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to create the service' { + Assert-MockCalled -CommandName 'New-Service' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to remove the service' { + Assert-MockCalled -CommandName 'Remove-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change the service path' { + Assert-MockCalled -CommandName 'Set-ServicePath' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change the service properties' { + Assert-MockCalled -CommandName 'Set-ServiceProperty' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to start the service' { + Assert-MockCalled -CommandName 'Start-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to stop or restart the service' { + Assert-MockCalled -CommandName 'Stop-ServiceWithTimeout' -Times 0 -Scope 'Context' } } - Context 'Service exists and should, should be running, path needs change' { - # Mocks that should be called - Mock -CommandName Test-StartupType ` - -Verifiable - - Mock -CommandName Test-ServiceExists ` - -MockWith { $true } ` - -Verifiable - - Mock -CommandName Compare-ServicePath ` - -MockWith { $false } ` - -Verifiable - - Mock -CommandName Write-WriteProperty ` - -MockWith { $false } ` - -Verifiable - - Mock -CommandName Start-ServiceResource ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName Stop-ServiceResource - Mock -CommandName New-Service - Mock -CommandName Remove-Service - - It 'Should not throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - $Splat.Path = 'c:\NewServicePath.exe' - { Set-TargetResource @Splat } | Should Not Throw + Context 'Service does not exist, Ensure set to Present, and Path not specified' { + $setTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Present' } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Start-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName Stop-ServiceResource -Exactly 0 - Assert-MockCalled -CommandName Remove-Service -Exactly 0 - Assert-MockCalled -CommandName New-Service -Exactly 0 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 1 - Assert-MockCalled -CommandName Write-WriteProperty -Exactly 1 + It 'Should throw an error for the missing path' { + $expectedErrorMessage = $script:localizedData.ServiceDoesNotExistPathMissingError -f $script:testServiceName + { Set-TargetResource @setTargetResourceParameters } | Should Throw $expectedErrorMessage } } - Context 'Service exists and should, should be running but needs restart, all parameters passed' { - # Mocks that should be called - Mock -CommandName Test-StartupType ` - -Verifiable - - Mock -CommandName Test-ServiceExists ` - -MockWith { $true } ` - -Verifiable - - Mock ` - -CommandName Compare-ServicePath ` - -MockWith { $true } ` - -Verifiable - - Mock -CommandName Write-WriteProperty ` - -MockWith { $true } ` - -Verifiable - - Mock ` - -CommandName Start-ServiceResource ` - -Verifiable - - Mock -CommandName Stop-ServiceResource ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName New-Service - Mock -CommandName Remove-Service - - It 'Should not throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - { Set-TargetResource @Splat } | Should Not Throw + Context 'Service does not exist, Ensure set to Present, and Path specified' { + $setTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Present' + Path = 'FakePath' } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Start-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName Stop-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName Remove-Service -Exactly 0 - Assert-MockCalled -CommandName New-Service -Exactly 0 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 1 - Assert-MockCalled -CommandName Write-WriteProperty -Exactly 1 + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should create the service' { + Assert-MockCalled -CommandName 'New-Service' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name -and $BinaryPathName -eq $setTargetResourceParameters.Path } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to remove the service' { + Assert-MockCalled -CommandName 'Remove-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change the service path' { + Assert-MockCalled -CommandName 'Set-ServicePath' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change the service properties' { + Assert-MockCalled -CommandName 'Set-ServiceProperty' -Times 0 -Scope 'Context' + } + + It 'Should start the service' { + Assert-MockCalled -CommandName 'Start-ServiceWithTimeout' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to stop or restart the service' { + Assert-MockCalled -CommandName 'Stop-ServiceWithTimeout' -Times 0 -Scope 'Context' } } - Context 'Service exists and should, should be stopped, all parameters passed' { - # Mocks that should be called - Mock -CommandName Test-StartupType ` - -Verifiable - - Mock -CommandName Test-ServiceExists ` - -MockWith { $true } ` - -Verifiable - - Mock -CommandName Compare-ServicePath ` - -MockWith { $true } ` - -Verifiable - - Mock -CommandName Write-WriteProperty ` - -MockWith { $false } ` - -Verifiable - - Mock -CommandName Stop-ServiceResource ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName New-Service - Mock -CommandName Remove-Service - Mock -CommandName Start-ServiceResource - - It 'Should not throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - $Splat.State = 'Stopped' - { Set-TargetResource @Splat } | Should Not Throw + Context 'Service does not exist, Ensure set to Present, State set to Running, and all parameters except Credential specified' { + $setTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Present' + Path = 'FakePath' + StartupType = 'Automatic' + BuiltInAccount = 'LocalSystem' + DesktopInteract = $true + State = 'Running' + DisplayName = 'TestDisplayName' + Description = 'Test device description' + Dependencies = @( 'TestServiceDependency1', 'TestServiceDependency2' ) } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Start-ServiceResource -Exactly 0 - Assert-MockCalled -CommandName Stop-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName Remove-Service -Exactly 0 - Assert-MockCalled -CommandName New-Service -Exactly 0 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 1 - Assert-MockCalled -CommandName Write-WriteProperty -Exactly 1 + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw + } + + It 'Should check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $State -eq $setTargetResourceParameters.State } -Times 1 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should create the service' { + Assert-MockCalled -CommandName 'New-Service' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name -and $BinaryPathName -eq $setTargetResourceParameters.Path } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to remove the service' { + Assert-MockCalled -CommandName 'Remove-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change the service path' { + Assert-MockCalled -CommandName 'Set-ServicePath' -Times 0 -Scope 'Context' + } + + It 'Should change all service properties except Credential' { + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and $DisplayName -eq $setTargetResourceParameters.DisplayName -and $Description -eq $setTargetResourceParameters.Description -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) } -Times 1 -Scope 'Context' + } + + It 'Should start the service' { + Assert-MockCalled -CommandName 'Start-ServiceWithTimeout' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to stop or restart the service' { + Assert-MockCalled -CommandName 'Stop-ServiceWithTimeout' -Times 0 -Scope 'Context' } } - Context 'Service exists and should, State is Ignore, all parameters passed' { - # Mocks that should be called - Mock -CommandName Test-StartupType ` - -Verifiable - - Mock -CommandName Test-ServiceExists ` - -MockWith { $true } ` - -Verifiable - - Mock -CommandName Compare-ServicePath ` - -MockWith { $true } ` - -Verifiable - - Mock -CommandName Write-WriteProperty ` - -MockWith { $false } ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName New-Service - Mock -CommandName Remove-Service - Mock -CommandName Start-ServiceResource - Mock -CommandName Stop-ServiceResource - - It 'Should not throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - $Splat.State = 'Ignore' - { Set-TargetResource @Splat } | Should Not Throw + Context 'Service does not exist, Ensure set to Present, State set to Stopped, and all parameters except BuiltInAccount specified' { + $setTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Present' + Path = 'FakePath' + StartupType = 'Disabled' + Credential = $script:testCredential1 + DesktopInteract = $true + State = 'Stopped' + DisplayName = 'TestDisplayName' + Description = 'Test device description' + Dependencies = @( 'TestServiceDependency1', 'TestServiceDependency2' ) } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Start-ServiceResource -Exactly 0 - Assert-MockCalled -CommandName Stop-ServiceResource -Exactly 0 - Assert-MockCalled -CommandName Remove-Service -Exactly 0 - Assert-MockCalled -CommandName New-Service -Exactly 0 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 1 - Assert-MockCalled -CommandName Write-WriteProperty -Exactly 1 + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw + } + + It 'Should check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $State -eq $setTargetResourceParameters.State } -Times 1 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should create the service' { + Assert-MockCalled -CommandName 'New-Service' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name -and $BinaryPathName -eq $setTargetResourceParameters.Path } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to remove the service' { + Assert-MockCalled -CommandName 'Remove-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change the service path' { + Assert-MockCalled -CommandName 'Set-ServicePath' -Times 0 -Scope 'Context' + } + + It 'Should change all service properties except BuiltInAccount' { + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Credential -DifferenceObject $Credential) -and $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and $DisplayName -eq $setTargetResourceParameters.DisplayName -and $Description -eq $setTargetResourceParameters.Description -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to start the service' { + Assert-MockCalled -CommandName 'Start-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should stop the service' { + Assert-MockCalled -CommandName 'Stop-ServiceWithTimeout' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' } } - Context 'Service does not exist but should' { - # Mocks that should be called - Mock -CommandName Test-StartupType ` - -Verifiable + $testService = @{ + Name = 'TestServiceName' + DisplayName = 'TestDisplayName' + Status = 'TestServiceStatus' + StartType = 'TestServiceStartType' + ServicesDependedOn = @( + @{ + Name = 'TestServiceDependency1' + }, + @{ + Name = 'TestServiceDependency2' + } + ) + } - Mock -CommandName Test-ServiceExists ` - -MockWith { $false } ` - -Verifiable + Mock -CommandName 'Get-Service' -MockWith { return $testService } - Mock -CommandName New-Service ` - -Verifiable - - Mock -CommandName Write-WriteProperty ` - -MockWith { $false } ` - -Verifiable - - Mock -CommandName Start-ServiceResource ` - -MockWith { $false } ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName Compare-ServicePath - Mock -CommandName Remove-Service - Mock -CommandName Stop-ServiceResource - - It 'Should not throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - { Set-TargetResource @Splat } | Should Not Throw + Context 'Service exists and Ensure set to Absent' { + $setTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Absent' } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Start-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName Stop-ServiceResource -Exactly 0 - Assert-MockCalled -CommandName Remove-Service -Exactly 0 - Assert-MockCalled -CommandName New-Service -Exactly 1 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 0 - Assert-MockCalled -CommandName Write-WriteProperty -Exactly 1 + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to create the service' { + Assert-MockCalled -CommandName 'New-Service' -Times 0 -Scope 'Context' + } + + It 'Should stop the service' { + Assert-MockCalled -CommandName 'Stop-ServiceWithTimeout' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should remove the service' { + Assert-MockCalled -CommandName 'Remove-ServiceWithTimeout' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to change the service path' { + Assert-MockCalled -CommandName 'Set-ServicePath' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change the service properties' { + Assert-MockCalled -CommandName 'Set-ServiceProperty' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to start the service' { + Assert-MockCalled -CommandName 'Start-ServiceWithTimeout' -Times 0 -Scope 'Context' } } - Context 'Service does not exist but should, but no path specified' { - # Mocks that should be called - Mock -CommandName Test-StartupType ` - -Verifiable - - Mock -CommandName Test-ServiceExists ` - -MockWith { $false } ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName New-Service - Mock -CommandName Compare-ServicePath - Mock -CommandName Start-ServiceResource - Mock -CommandName Remove-Service - Mock -CommandName Stop-ServiceResource - Mock -CommandName Write-WriteProperty - - $errorRecord = Get-InvalidArgumentRecord ` - -ErrorId 'ServiceDoesNotExistPathMissingError' ` - -ErrorMessage ($script:localizedData.ServiceDoesNotExistPathMissingError ` - -f $script:testServiceName) - - It 'Should throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - $Splat.Remove('Path') - { Set-TargetResource @Splat } | Should Throw $errorRecord + Context 'Service exists and Ensure set to Present' { + $setTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Present' } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Start-ServiceResource -Exactly 0 - Assert-MockCalled -CommandName Stop-ServiceResource -Exactly 0 - Assert-MockCalled -CommandName Remove-Service -Exactly 0 - Assert-MockCalled -CommandName New-Service -Exactly 0 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 0 - Assert-MockCalled -CommandName Write-WriteProperty -Exactly 0 + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to create the service' { + Assert-MockCalled -CommandName 'New-Service' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to remove the service' { + Assert-MockCalled -CommandName 'Remove-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change the service path' { + Assert-MockCalled -CommandName 'Set-ServicePath' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change the service properties' { + Assert-MockCalled -CommandName 'Set-ServiceProperty' -Times 0 -Scope 'Context' + } + + It 'Should start the service' { + Assert-MockCalled -CommandName 'Start-ServiceWithTimeout' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to stop or restart the service' { + Assert-MockCalled -CommandName 'Stop-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + } + + Context 'Service exists, Ensure set to Present, and Path specified' { + $setTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Present' + Path = 'TestPath' + } + + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to create the service' { + Assert-MockCalled -CommandName 'New-Service' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to remove the service' { + Assert-MockCalled -CommandName 'Remove-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should change the service path' { + Assert-MockCalled -CommandName 'Set-ServicePath' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $Path -eq $setTargetResourceParameters.Path } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to change the service properties' { + Assert-MockCalled -CommandName 'Set-ServiceProperty' -Times 0 -Scope 'Context' + } + + It 'Should stop the service to restart it' { + Assert-MockCalled -CommandName 'Stop-ServiceWithTimeout' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should start the service' { + Assert-MockCalled -CommandName 'Start-ServiceWithTimeout' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + } + + Context 'Service exists, Ensure set to Present, State set to Stopped, and all parameters except Credential specified' { + $setTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Present' + Path = 'FakePath' + StartupType = 'Automatic' + BuiltInAccount = 'LocalSystem' + DesktopInteract = $true + State = 'Stopped' + DisplayName = 'TestDisplayName' + Description = 'Test device description' + Dependencies = @( 'TestServiceDependency1', 'TestServiceDependency2' ) + } + + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw + } + + It 'Should check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $State -eq $setTargetResourceParameters.State } -Times 1 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to create the service' { + Assert-MockCalled -CommandName 'New-Service' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to remove the service' { + Assert-MockCalled -CommandName 'Remove-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should change the service path' { + Assert-MockCalled -CommandName 'Set-ServicePath' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $Path -eq $setTargetResourceParameters.Path } -Times 1 -Scope 'Context' + } + + It 'Should change all service properties except Credential' { + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and $DisplayName -eq $setTargetResourceParameters.DisplayName -and $Description -eq $setTargetResourceParameters.Description -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to start the service' { + Assert-MockCalled -CommandName 'Start-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should stop the service' { + Assert-MockCalled -CommandName 'Stop-ServiceWithTimeout' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + } + + Context 'Service exists, Ensure set to Present, State set to Ignore, and all parameters except Path and BuiltInAccount specified' { + $setTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Present' + StartupType = 'Manual' + Credential = $script:testCredential1 + DesktopInteract = $true + State = 'Ignore' + DisplayName = 'TestDisplayName' + Description = 'Test device description' + Dependencies = @( 'TestServiceDependency1', 'TestServiceDependency2' ) + } + + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw + } + + It 'Should check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $State -eq $setTargetResourceParameters.State } -Times 1 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to create the service' { + Assert-MockCalled -CommandName 'New-Service' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to remove the service' { + Assert-MockCalled -CommandName 'Remove-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change the service path' { + Assert-MockCalled -CommandName 'Set-ServicePath' -Times 0 -Scope 'Context' + } + + It 'Should change all service properties except BuiltInAccount' { + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and $DisplayName -eq $setTargetResourceParameters.DisplayName -and $Description -eq $setTargetResourceParameters.Description -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to start the service' { + Assert-MockCalled -CommandName 'Start-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to stop the service' { + Assert-MockCalled -CommandName 'Stop-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + } + + Context 'Service exists, Ensure set to Present, and DesktopInteract specified' { + $setTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Present' + DesktopInteract = $true + } + + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to create the service' { + Assert-MockCalled -CommandName 'New-Service' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to remove the service' { + Assert-MockCalled -CommandName 'Remove-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change the service path' { + Assert-MockCalled -CommandName 'Set-ServicePath' -Times 0 -Scope 'Context' + } + + It 'Should change only DesktopInteract service property' { + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract } -Times 1 -Scope 'Context' + } + + It 'Should start the service' { + Assert-MockCalled -CommandName 'Start-ServiceWithTimeout' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to stop the service' { + Assert-MockCalled -CommandName 'Stop-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + } + + Mock -CommandName 'Set-ServicePath' -MockWith { return $false } + + Context 'Service exists, Ensure set to Present, and matching Path to service path specified' { + $setTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Present' + Path = 'TestPath' + } + + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to create the service' { + Assert-MockCalled -CommandName 'New-Service' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to remove the service' { + Assert-MockCalled -CommandName 'Remove-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should attempt to change the service path' { + Assert-MockCalled -CommandName 'Set-ServicePath' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $Path -eq $setTargetResourceParameters.Path } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to change the service properties' { + Assert-MockCalled -CommandName 'Set-ServiceProperty' -Times 0 -Scope 'Context' + } + + It 'Should not stop the service to restart it' { + Assert-MockCalled -CommandName 'Stop-ServiceWithTimeout' -Times 0 -Scope 'Context' + } + + It 'Should start the service' { + Assert-MockCalled -CommandName 'Start-ServiceWithTimeout' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name } -Times 1 -Scope 'Context' } } } - Describe "$script:DscResourceName\Test-TargetResource" { - # Mocks that should be called - Mock -CommandName Test-ServiceExists ` - -MockWith { $true } ` - -Verifiable + Describe 'Service\Test-TargetResource' { + Mock -CommandName 'Assert-NoStartupTypeStateConflict' -MockWith { } + Mock -CommandName 'Get-TargetResource' -MockWith { + return @{ + Name = $script:testServiceName + Ensure = 'Absent' + } + } + Mock -CommandName 'Test-PathsMatch' -MockWith { return $true } + Mock -CommandName 'ConvertTo-StartName' -MockWith { return $Username } - Mock -CommandName Test-StartupType ` - -Verifiable + Context 'Both BuiltInAccount and Credential specified' { + $testTargetResourceParameters = @{ + Name = $script:testServiceName + BuiltInAccount = 'LocalSystem' + Credential = $script:testCredential1 + } - Mock -CommandName Get-ServiceResource ` - -MockWith { $script:testServiceMockRunning } ` - -Verifiable + It 'Should throw an error for BuiltInAccount and Credential conflict' { + $expectedErrorMessage = $script:localizedData.BuiltInAccountAndCredentialSpecified -f $testTargetResourceParameters.Name + { Test-TargetResource @testTargetResourceParameters } | Should Throw $expectedErrorMessage + } + } - Mock -CommandName Get-Win32ServiceObject ` - -MockWith { $script:testWin32ServiceMockRunningLocalSystem } ` - -Verifiable + Context 'Service does not exist and Ensure set to Absent' { + $testTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Absent' + } + + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } - Mock -CommandName Compare-ServicePath ` - -MockWith { $true } ` - -Verifiable + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } - Mock -CommandName Test-UserName ` - -MockWith { $true } ` - -Verifiable + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-TargetResource' -ParameterFilter { $Name -eq $testTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } - Context 'Service exists and should, and all parameters match' { - It 'Should not throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - { $script:result = Test-TargetResource @Splat } | Should Not Throw + It 'Should not attempt to test if the service path matches the specified path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to convert a credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -Times 0 -Scope 'Context' } It 'Should return true' { - $script:result | Should Be $true - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Get-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 1 + Test-TargetResource @testTargetResourceParameters | Should Be $true } } - Context 'Service exists and should, path mistmatches' { - # Mocks that should be called - Mock -CommandName Compare-ServicePath ` - -MockWith { $false } ` - -Verifiable + Context 'Service does not exist and Ensure set to Present' { + $testTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Present' + } + + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } - # Mocks that should not be called - Mock -CommandName Test-UserName + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } - It 'Should not throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - $Splat.Path = 'c:\ANewPath.exe' - { $script:result = Test-TargetResource @Splat } | Should Not Throw + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-TargetResource' -ParameterFilter { $Name -eq $testTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to test if the service path matches the specified path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to convert a credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -Times 0 -Scope 'Context' } It 'Should return false' { - $script:result | Should Be $false - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Get-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 0 + Test-TargetResource @testTargetResourceParameters | Should Be $false } } - Context 'Service exists and should, startup type mistmatches' { - # Mocks that should be called - Mock -CommandName Test-UserName ` - -MockWith { $true } ` - -Verifiable + $serviceResourceWithAllProperties = @{ + Name = $script:testServiceName + Ensure = 'Present' + StartupType = 'Automatic' + BuiltInAccount = 'LocalSystem' + DesktopInteract = $false + State = 'Running' + Path = 'TestPath' + DisplayName = 'TestDisplayName' + Description = 'Test service description' + Dependencies = @( 'TestServiceDependency1', 'TestServiceDependency2' ) + } - It 'Should not throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - $Splat.StartupType = 'Manual' - { $script:result = Test-TargetResource @Splat } | Should Not Throw + Mock -CommandName 'Get-TargetResource' -MockWith { return $serviceResourceWithAllProperties } + + Context 'Service exists and Ensure set to Absent' { + $testTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Absent' + } + + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-TargetResource' -ParameterFilter { $Name -eq $testTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to test if the service path matches the specified path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to convert a credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -Times 0 -Scope 'Context' } It 'Should return false' { - $script:result | Should Be $false - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Get-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 1 + Test-TargetResource @testTargetResourceParameters | Should Be $false } } - Context 'Service exists and should, credential mistmatches' { - # Mocks that should be called - Mock -CommandName Test-UserName ` - -MockWith { $false } ` - -Verifiable - - It 'Should not throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - $Splat.Credential = $script:testNewCredential - { $script:result = Test-TargetResource @Splat } | Should Not Throw + Context 'Service exists and Ensure set to Present' { + $testTargetResourceParameters = @{ + Name = $script:testServiceName + Ensure = 'Present' + } + + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParameters } | Should Not Throw } - It 'Should return false' { - $script:result | Should Be $false + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Get-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 1 - } - } - - Context 'Service exists and should, is running but should be stopped' { - # Mocks that should be called - Mock -CommandName Compare-ServicePath ` - -MockWith { $true } ` - -Verifiable - - Mock -CommandName Test-UserName ` - -MockWith { $true } ` - -Verifiable - - It 'Should not throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - $Splat.State = 'Stopped' - { $script:result = Test-TargetResource @Splat } | Should Not Throw + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-TargetResource' -ParameterFilter { $Name -eq $testTargetResourceParameters.Name } -Times 1 -Scope 'Context' } - It 'Should return false' { - $script:result | Should Be $false + It 'Should not attempt to test if the service path matches the specified path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -Times 0 -Scope 'Context' } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Get-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 1 - } - } - - Context 'Service exists and should, everything matches and State is set to Ignore' { - # Mocks that should be called - Mock -CommandName Compare-ServicePath ` - -MockWith { $true } ` - -Verifiable - - Mock -CommandName Test-UserName ` - -MockWith { $true } ` - -Verifiable - - It 'Should not throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - $Splat.State = 'Ignore' - { $script:result = Test-TargetResource @Splat } | Should Not Throw + It 'Should not attempt to convert a credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -Times 0 -Scope 'Context' } It 'Should return true' { - $script:result | Should Be $true - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Get-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 1 + Test-TargetResource @testTargetResourceParameters | Should Be $true } } - Context 'Service exists and should not' { - # Mocks that should not be called - Mock -CommandName Compare-ServicePath - Mock -CommandName Test-UserName - Mock -CommandName Get-ServiceResource - Mock -CommandName Get-Win32ServiceObject + Context 'Service exists, Ensure set to Present, and all matching parameters specified except Credential' { + $testTargetResourceParameters = $serviceResourceWithAllProperties + + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } - It 'Should not throw an exception' { - $Splat = $script:splatServiceExistsAutomatic.Clone() - $Splat.Ensure = 'Absent' - { $script:result = Test-TargetResource @Splat } | Should Not Throw + It 'Should check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -ParameterFilter { $ServiceName -eq $testTargetResourceParameters.Name -and $StartupType -eq $testTargetResourceParameters.StartupType -and $State -eq $testTargetResourceParameters.State } -Times 1 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-TargetResource' -ParameterFilter { $Name -eq $testTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should test if the service path matches the specified path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -ParameterFilter { $ExpectedPath -eq $testTargetResourceParameters.Path -and $ActualPath -eq $serviceResourceWithAllProperties.Path } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to convert a credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -Times 0 -Scope 'Context' + } + + It 'Should return true' { + Test-TargetResource @testTargetResourceParameters | Should Be $true + } + } + + $mismatchingParameters = @{ + StartupType = 'Manual' + BuiltInAccount = 'NetworkService' + DesktopInteract = $true + State = 'Stopped' + DisplayName = 'MismatchingDisplayName' + Description = 'Mismatching service description' + Dependencies = @( 'TestServiceDependency3', 'TestServiceDependency4' ) + } + + foreach ($mismatchingParameterName in $mismatchingParameters.Keys) + { + Context "Service exists, Ensure set to Present, and mismatching $mismatchingParameterName specified" { + $testTargetResourceParameters = @{ + Name = $serviceResourceWithAllProperties.Name + Ensure = 'Present' + $mismatchingParameterName = $mismatchingParameters[$mismatchingParameterName] + } + + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } + + + + if ($mismatchingParameterName -eq 'StartupType') + { + It 'Should check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -ParameterFilter { $ServiceName -eq $testTargetResourceParameters.Name -and $StartupType -eq $testTargetResourceParameters.StartupType -and $State -eq 'Running' } -Times 1 -Scope 'Context' + } + } + else + { + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-TargetResource' -ParameterFilter { $Name -eq $testTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not test if the service path matches the specified path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to convert a credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -Times 0 -Scope 'Context' + } + + It 'Should return false' { + Test-TargetResource @testTargetResourceParameters | Should Be $false + } + } + } + + Context 'Service exists, Ensure set to Present, and State is set to Ignore' { + $testTargetResourceParameters = @{ + Name = $serviceResourceWithAllProperties.Name + Ensure = 'Present' + State = 'Ignore' + } + + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-TargetResource' -ParameterFilter { $Name -eq $testTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not test if the service path matches the specified path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to convert a credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -Times 0 -Scope 'Context' + } + + It 'Should return true' { + Test-TargetResource @testTargetResourceParameters | Should Be $true + } + } + + $serviceResourceWithCustomBuiltInAccount = @{ + Name = $script:testServiceName + Ensure = 'Present' + State = 'Running' + BuiltInAccount = $script:testCredential1.UserName + DisplayName = 'TestDisplayName' + Description = 'Test service description' + Dependencies = @( 'TestServiceDependency1', 'TestServiceDependency2' ) + } + + Mock -CommandName 'Get-TargetResource' -MockWith { return $serviceResourceWithCustomBuiltInAccount } + + Context 'Service exists, Ensure set to Present, and matching Credential specified' { + $testTargetResourceParameters = @{ + Name = $serviceResourceWithCustomBuiltInAccount.Name + Ensure = 'Present' + Credential = $script:testCredential1 + } + + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-TargetResource' -ParameterFilter { $Name -eq $testTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not test if the service path matches the specified path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -Times 0 -Scope 'Context' + } + + It 'Should convert the credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -ParameterFilter { $Username -eq $script:testCredential1.UserName } -Times 1 -Scope 'Context' + } + + It 'Should return true' { + Test-TargetResource @testTargetResourceParameters | Should Be $true + } + } + + Context 'Service exists, Ensure set to Present, and mismatching Credential specified' { + $testTargetResourceParameters = @{ + Name = $serviceResourceWithCustomBuiltInAccount.Name + Ensure = 'Present' + Credential = $script:testCredential2 + } + + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-TargetResource' -ParameterFilter { $Name -eq $testTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not test if the service path matches the specified path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -Times 0 -Scope 'Context' + } + + It 'Should convert the credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -ParameterFilter { $Username -eq $script:testCredential2.UserName } -Times 1 -Scope 'Context' } It 'Should return false' { - $script:result | Should Be $false + Test-TargetResource @testTargetResourceParameters | Should Be $false + } + } + + $allowedEmptyPropertyNames = @( 'DisplayName', 'Description', 'Dependencies' ) + + foreach ($allowedEmptyPropertyName in $allowedEmptyPropertyNames) + { + Context "Service exists, Ensure set to Present, $allowedEmptyPropertyName specified as empty" { + $testTargetResourceParameters = @{ + Name = $serviceResourceWithCustomBuiltInAccount.Name + Ensure = 'Present' + } + + if ($allowedEmptyPropertyName -eq 'Dependencies') + { + $testTargetResourceParameters[$allowedEmptyPropertyName] = @() + } + else + { + $testTargetResourceParameters[$allowedEmptyPropertyName] = '' + } + + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-TargetResource' -ParameterFilter { $Name -eq $testTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not test if the service path matches the specified path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to convert a credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -Times 0 -Scope 'Context' + } + + It 'Should return false' { + Test-TargetResource @testTargetResourceParameters | Should Be $false + } + } + } + + $serviceResourceWithNullProperties = @{ + Name = $script:testServiceName + Ensure = 'Present' + Path = 'TestPath' + State = 'Running' + } + + foreach ($nullPropertyName in $allowedEmptyPropertyNames) + { + $serviceResourceWithNullProperties[$nullPropertyName] = $null + } + + Mock -CommandName 'Get-TargetResource' -MockWith { return $serviceResourceWithNullProperties } + + foreach ($nullPropertyName in $allowedEmptyPropertyNames) + { + Context "Service exists but DisplayName, Description, and Dependencies are null, Ensure set to Present, $nullPropertyName specified" { + $testTargetResourceParameters = @{ + Name = $serviceResourceWithNullProperties.Name + Ensure = 'Present' + $nullPropertyName = 'Something' + } + + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-TargetResource' -ParameterFilter { $Name -eq $testTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not test if the service path matches the specified path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to convert a credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -Times 0 -Scope 'Context' + } + + It 'Should return false' { + Test-TargetResource @testTargetResourceParameters | Should Be $false + } } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Get-ServiceResource -Exactly 0 - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 0 - Assert-MockCalled -CommandName Test-StartupType -Exactly 1 - Assert-MockCalled -CommandName Compare-ServicePath -Exactly 0 - Assert-MockCalled -CommandName Test-UserName -Exactly 0 + Context "Service exists but DisplayName, Description, and Dependencies are null, Ensure set to Present, $nullPropertyName not specified" { + $testTargetResourceParameters = @{ + Name = $serviceResourceWithNullProperties.Name + Ensure = 'Present' + } + + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-TargetResource' -ParameterFilter { $Name -eq $testTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should not test if the service path matches the specified path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to convert a credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -Times 0 -Scope 'Context' + } + + It 'Should return true' { + Test-TargetResource @testTargetResourceParameters | Should Be $true + } + } + } + + Mock -CommandName 'Test-PathsMatch' -MockWith { return $false } + + Context 'Service exists, Ensure set to Present, and mismatching Path specified' { + $testTargetResourceParameters = @{ + Name = $serviceResourceWithCustomBuiltInAccount.Name + Ensure = 'Present' + Path = 'Mismatching path' + } + + It 'Should not throw' { + { Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } + + It 'Should not check for a startup type and state conflict' { + Assert-MockCalled -CommandName 'Assert-NoStartupTypeStateConflict' -Times 0 -Scope 'Context' + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-TargetResource' -ParameterFilter { $Name -eq $testTargetResourceParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should test if the service path matches the specified path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -ParameterFilter { $ExpectedPath -eq $testTargetResourceParameters.Path -and $ActualPath -eq $serviceResourceWithNullProperties.Path } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to convert a credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -Times 0 -Scope 'Context' + } + + It 'Should return false' { + Test-TargetResource @testTargetResourceParameters | Should Be $false } } } - Describe "$script:DscResourceName\Test-StartupType" { - Context 'Service is stopped, startup is automatic' { - $errorRecord = Get-InvalidArgumentRecord ` - -ErrorId 'CannotStopServiceSetToStartAutomatically' ` - -ErrorMessage ($script:localizedData.CannotStopServiceSetToStartAutomatically ` - -f $script:testServiceName) + Describe 'Service\Get-ServiceCimInstance' { + Mock -CommandName 'Get-CimInstance' -MockWith { } - It 'Should throw CannotStopServiceSetToStartAutomatically exception' { - { Test-StartupType ` - -Name $script:testServiceName ` - -StartupType 'Automatic' ` - -State 'Stopped' ` - } | Should Throw $errorRecord + Context 'Service does not exist' { + It 'Should not throw' { + { Get-ServiceCimInstance -ServiceName $script:testServiceName } | Should Not Throw + } + + It 'Should retrieve the CIM instance of the service with the given name' { + Assert-MockCalled -CommandName 'Get-CimInstance' -ParameterFilter {$ClassName -ieq 'Win32_Service' -and $Filter.Contains($script:testServiceName)} -Times 1 -Scope 'Context' + } + + It 'Should return null' { + Get-ServiceCimInstance -ServiceName $script:testServiceName | Should Be $null } } - Context 'Service is stopped, startup is not automatic' { - It 'Should not throw an exception' { - { Test-StartupType ` - -Name $script:testServiceName ` - -StartupType 'Disabled' ` - -State 'Stopped' ` - } | Should Not Throw - } - } + $testCimInstance = 'TestCimInstance' - Context 'Service is running, startup is disabled' { - $errorRecord = Get-InvalidArgumentRecord ` - -ErrorId 'CannotStartAndDisable' ` - -ErrorMessage ($script:localizedData.CannotStartAndDisable -f $script:testServiceName) + Mock -CommandName 'Get-CimInstance' -MockWith { return $testCimInstance } - It 'Should throw CannotStartAndDisable exception' { - { Test-StartupType ` - -Name $script:testServiceName ` - -StartupType 'Disabled' ` - -State 'Running' ` - } | Should Throw $errorRecord - } - } - - Context 'Service is running, startup is not disabled' { - It 'Should not throw exception' { - { Test-StartupType ` - -Name $script:testServiceName ` - -StartupType 'Manual' ` - -State 'Running' ` - } | Should Not Throw - } - } - - Context 'State is Ignore' { - It 'Should not throw exception for Disabled' { - { Test-StartupType ` - -Name $script:testServiceName ` - -StartupType 'Disabled' ` - -State 'Ignore' ` - } | Should Not Throw + Context 'Service exists' { + It 'Should not throw' { + { Get-ServiceCimInstance -ServiceName $script:testServiceName } | Should Not Throw } - It 'Should not throw exception for Automatic' { - { Test-StartupType ` - -Name $script:testServiceName ` - -StartupType 'Automatic' ` - -State 'Ignore' ` - } | Should Not Throw + It 'Should retrieve the CIM instance of the service with the given name' { + Assert-MockCalled -CommandName 'Get-CimInstance' -ParameterFilter {$ClassName -ieq 'Win32_Service' -and $Filter.Contains($script:testServiceName)} -Times 1 -Scope 'Context' + } + + It 'Should return the retrieved CIM instance' { + Get-ServiceCimInstance -ServiceName $script:testServiceName | Should Be $testCimInstance } } } - Describe "$script:DscResourceName\ConvertTo-StartModeString" { - Context 'StartupType is Automatic' { - It 'Should return Auto' { - ConvertTo-StartModeString -StartupType 'Automatic' | Should Be 'Auto' - } - } - - Context 'StartupType is Disabled' { - It 'Should return Disabled' { - ConvertTo-StartModeString -StartupType 'Disabled' | Should Be 'Disabled' - } - } - } - - Describe "$script:DscResourceName\ConvertTo-StartupTypeString" { - Context 'StartupType is Auto' { + Describe 'Service\ConvertTo-StartupTypeString' { + Context 'StartupType specifed as Auto' { It 'Should return Automatic' { ConvertTo-StartupTypeString -StartMode 'Auto' | Should Be 'Automatic' } } - Context 'StartupType is Disabled' { + Context 'StartupType specifed as Manual' { + It 'Should return Manual' { + ConvertTo-StartupTypeString -StartMode 'Manual' | Should Be 'Manual' + } + } + + Context 'StartupType specifed as Disabled' { It 'Should return Disabled' { ConvertTo-StartupTypeString -StartMode 'Disabled' | Should Be 'Disabled' } } } - Describe "$script:DscResourceName\Get-Win32ServiceObject" { - Context 'Service exists' { - Mock -CommandName Get-CimInstance ` - -MockWith { $script:testWin32ServiceMockRunningLocalSystem } ` - -Verifiable + Describe 'Service\Assert-NoStartupTypeStateConflict' { + $stateValues = @( 'Running', 'Stopped', 'Ignore' ) + $startupTypeValues = @( 'Manual', 'Automatic', 'Disabled' ) - It 'Should not throw an exception' { - { $script:result = Get-Win32ServiceObject ` - -Name $script:testServiceName } | Should Not Throw - } + foreach ($startupTypeValue in $startupTypeValues) + { + foreach ($stateValue in $stateValues) + { + Context "StartupType specified as $startupTypeValue and State specified as $stateValue" { + $assertNoStartupTypeStateConflictParameters = @{ + ServiceName = $script:testServiceName + StartupType = $startupTypeValue + State = $stateValue + } - It 'Should return expected hash table' { - $script:result = $script:testWin32ServiceMockRunningLocalSystem - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-CimInstance -Exactly 1 - } - } - - Context 'Service does not exist' { - Mock -CommandName Get-CimInstance ` - -Verifiable - - It 'Should not throw an exception' { - { $script:result = Get-Win32ServiceObject ` - -Name $script:testServiceName } | Should Not Throw - } - - It 'Should return $null' { - $script:result | Should BeNullOrEmpty - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-CimInstance -Exactly 1 + if ($stateValue -eq 'Running' -and $startupTypeValue -eq 'Disabled') + { + It 'Should throw error for conflicting state and startup type' { + $errorMessage = $script:localizedData.StartupTypeStateConflict -f $assertNoStartupTypeStateConflictParameters.ServiceName, $startupTypeValue, $stateValue + { Assert-NoStartupTypeStateConflict @assertNoStartupTypeStateConflictParameters } | Should Throw $errorMessage + } + } + elseif ($stateValue -eq 'Stopped' -and $startupTypeValue -eq 'Automatic') + { + It 'Should throw error for conflicting state and startup type' { + $errorMessage = $script:localizedData.StartupTypeStateConflict -f $assertNoStartupTypeStateConflictParameters.ServiceName, $startupTypeValue, $stateValue + { Assert-NoStartupTypeStateConflict @assertNoStartupTypeStateConflictParameters } | Should Throw $errorMessage + } + } + else + { + It 'Should not throw' { + { Assert-NoStartupTypeStateConflict @assertNoStartupTypeStateConflictParameters } | Should Not Throw + } + } + } } } } - Describe "$script:DscResourceName\Set-ServiceStartMode" { - # Stub Functions for Mocking - function Invoke-CimMethod { param ( $InputObject, $MethodName, $Arguments ) } - - Context 'Current StartMode is set to Auto and should be' { - Mock -CommandName Invoke-CimMethod - - It 'Should not throw an exception' { - { Set-ServiceStartMode ` - -Win32ServiceObject $script:testWin32ServiceMockRunningLocalSystem ` - -StartupType $script:testServiceStartupType ` - } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 0 - } - } - - Context 'Current StartMode needs to be changed, and is changed successfully' { - Mock -CommandName Invoke-CimMethod ` - -MockWith { return @{ ReturnValue = 0 } } ` - -Verifiable - - It 'Should not throw an exception' { - { Set-ServiceStartMode ` - -Win32ServiceObject $script:testWin32ServiceMockRunningLocalSystem ` - -StartupType 'Manual' ` - } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 1 - } - } - - Context 'Current StartMode needs to be changed but an error occured' { - Mock -CommandName Invoke-CimMethod ` - -MockWith { return @{ ReturnValue = 99 } } ` - -Verifiable - - $innerMessage = ($script:localizedData.MethodFailed ` - -f 'Change', 'Win32_Service', '99' ) - $errorMessage = ($script:localizedData.ErrorChangingProperty ` - -f 'StartupType', $innerMessage) - $errorRecord = Get-InvalidArgumentRecord ` - -ErrorId 'ChangeStartupTypeFailed' ` - -ErrorMessage $errorMessage - - It 'Should throw an exception' { - { Set-ServiceStartMode ` - -Win32ServiceObject $script:testWin32ServiceMockRunningLocalSystem ` - -StartupType 'Manual' ` - } | Should Throw $errorMessage - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 1 - } - } - } - - Describe "$script:DscResourceName\Write-WriteProperty" { - # Stub Functions for Mocking - function Invoke-CimMethod { param ( $InputObject, $MethodName, $Arguments ) } - - # Mocks that should be called - Mock -CommandName Get-Win32ServiceObject ` - -MockWith { $script:testServiceStartupTypeWin32 } ` - -Verifiable - - Mock -CommandName Get-Service ` - -MockWith { $script:testServiceMockRunning } ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName Set-Service - - Context 'No parameters passed' { - It 'Should not throw an exception' { - { Write-WriteProperty -Name $script:testServiceName } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Get-Service -Exactly 1 - Assert-MockCalled -CommandName Set-Service -Exactly 0 - } - } - - Context 'Different DisplayName passed, will not trigger restart' { - # Mocks that should be called - Mock -CommandName Set-Service ` - -Verifiable - - It 'Should not throw an exception' { - { $script:Result = Write-WriteProperty ` - -Name $script:testServiceName ` - -DisplayName 'NewDisplayName' ` - } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Get-Service -Exactly 1 - Assert-MockCalled -CommandName Set-Service -Exactly 1 - } - } - - Context 'Different Description passed, will not trigger restart' { - # Mocks that should be called - Mock -CommandName Set-Service ` - -Verifiable - - It 'Should not throw an exception' { - { $script:Result = Write-WriteProperty ` - -Name $script:testServiceName ` - -Description 'NewDescription' ` - } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Get-Service -Exactly 1 - Assert-MockCalled -CommandName Set-Service -Exactly 1 - } - } - - Context 'Different Dependencies passed and set successfully, will not trigger restart' { - # Mocks that should be called - Mock -CommandName Invoke-CimMethod ` - -MockWith { @{ ReturnValue = 0 } } ` - -Verifiable - - It 'Should not throw an exception' { - { $script:Result = Write-WriteProperty ` - -Name $script:testServiceName ` - -Dependencies 'DepService1','DepService2' ` - } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Get-Service -Exactly 1 - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 1 - } - } - - Context 'Different Dependencies passed and set failed, will not trigger restart' { - # Mocks that should be called - Mock -CommandName Invoke-CimMethod ` - -MockWith { @{ ReturnValue = 99 } } ` - -Verifiable - - $innerMessage = ($script:localizedData.MethodFailed ` - -f 'Change','Win32_Service','99') - $errorMessage = ($script:localizedData.ErrorChangingProperty ` - -f 'Dependencies',$innerMessage) - $errorRecord = Get-InvalidArgumentRecord ` - -ErrorId 'ChangeCredentialFailed' ` - -ErrorMessage $errorMessage - - It 'Should throw an exception' { - { $script:Result = Write-WriteProperty ` - -Name $script:testServiceName ` - -Dependencies 'DepService1','DepService2' ` - } | Should Throw $errorRecord - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Get-Service -Exactly 1 - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 1 - } - } - - Context 'Path passed, will trigger restart' { - # Mocks that should be called - Mock -CommandName Write-BinaryProperty ` - -MockWith { $true } ` - -Verifiable - - It 'Should not throw an exception' { - { $script:Result = Write-WriteProperty ` - -Name $script:testServiceName ` - -Path 'c:\NewExecutable.exe' ` - } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Get-Service -Exactly 1 - Assert-MockCalled -CommandName Write-BinaryProperty -Exactly 1 - } - } - - Context 'StartupType passed, will not trigger restart' { - # Mocks that should be called - Mock -CommandName Set-ServiceStartMode ` - -Verifiable - - It 'Should not throw an exception' { - { $script:Result = Write-WriteProperty ` - -Name $script:testServiceName ` - -StartupType 'Manual' ` - } | Should Not Throw - } - - It 'Should return false' { - $script:Result | Should Be $false - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Get-Service -Exactly 1 - Assert-MockCalled -CommandName Set-ServiceStartMode -Exactly 1 - } - } - - Context 'Credential passed, will not trigger restart' { - # Mocks that should be called - Mock -CommandName Write-CredentialProperty ` - -Verifiable - - It 'Should not throw an exception' { - { $script:Result = Write-WriteProperty ` - -Name $script:testServiceName ` - -Credential $script:testCredential ` - } | Should Not Throw - } - - It 'Should return false' { - $script:Result | Should Be $false - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Get-Service -Exactly 1 - Assert-MockCalled -CommandName Write-CredentialProperty -Exactly 1 - } - } - - Context 'BuildinAccount passed, will not trigger restart' { - # Mocks that should be called - Mock -CommandName Write-CredentialProperty ` - -Verifiable - - It 'Should not throw an exception' { - { $script:Result = Write-WriteProperty ` - -Name $script:testServiceName ` - -BuiltInAccount 'LocalSystem' ` - } | Should Not Throw - } - - It 'Should return false' { - $script:Result | Should Be $false - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Get-Service -Exactly 1 - Assert-MockCalled -CommandName Write-CredentialProperty -Exactly 1 - } - } - - Context 'DesktopInteract passed, will not trigger restart' { - # Mocks that should be called - Mock -CommandName Write-CredentialProperty ` - -Verifiable - - It 'Should not throw an exception' { - { $script:Result = Write-WriteProperty ` - -Name $script:testServiceName ` - -DesktopInteract $true ` - } | Should Not Throw - } - - It 'Should return false' { - $script:Result | Should Be $false - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-Win32ServiceObject -Exactly 1 - Assert-MockCalled -CommandName Get-Service -Exactly 1 - Assert-MockCalled -CommandName Write-CredentialProperty -Exactly 1 - } - } - } - - Describe "$script:DscResourceName\Write-CredentialProperty" { - # Dummy Functions - function Invoke-CimMethod { param ( $InputObject, $MethodName, $Arguments ) } - - Context 'No parameters to be changed passed in' { - # Mocks that should not be called - Mock -CommandName Get-UserNameAndPassword - Mock -CommandName Test-UserName - Mock -CommandName Set-LogOnAsServicePolicy - Mock -CommandName Invoke-CimMethod - - It 'Should not throw an exception' { - { Write-CredentialProperty ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-UserNameAndPassword -Exactly 0 - Assert-MockCalled -CommandName Test-UserName -Exactly 0 - Assert-MockCalled -CommandName Set-LogOnAsServicePolicy -Exactly 0 - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 0 - } - } - - Context 'Desktop interact passed but does not need to be changed' { - # Mocks that should be called - Mock -CommandName Get-UserNameAndPassword ` - -MockWith { $null, $null } ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName Test-UserName - Mock -CommandName Set-LogOnAsServicePolicy - Mock -CommandName Invoke-CimMethod - - It 'Should not throw an exception' { - { Write-CredentialProperty ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -DesktopInteract $true ` - } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-UserNameAndPassword -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 0 - Assert-MockCalled -CommandName Set-LogOnAsServicePolicy -Exactly 0 - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 0 - } - } - - Context 'Desktop interact passed and does need to be changed' { - # Mocks that should be called - Mock -CommandName Get-UserNameAndPassword ` - -MockWith { $null, $null } ` - -Verifiable - - Mock -CommandName Invoke-CimMethod ` - -MockWith { @{ returnValue = 0 } } ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName Test-UserName - Mock -CommandName Set-LogOnAsServicePolicy - - It 'Should not throw an exception' { - { Write-CredentialProperty ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -DesktopInteract $false ` - } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-UserNameAndPassword -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 0 - Assert-MockCalled -CommandName Set-LogOnAsServicePolicy -Exactly 0 - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 1 - } - } - - Context 'Desktop interact passed and does need to be changed but fails' { - # Mocks that should be called - Mock -CommandName Get-UserNameAndPassword ` - -MockWith { $null,$null } ` - -Verifiable - - Mock -CommandName Invoke-CimMethod ` - -MockWith { @{ returnValue = 99 } } ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName Test-UserName - Mock -CommandName Set-LogOnAsServicePolicy - - $innerMessage = ($script:localizedData.MethodFailed ` - -f 'Change','Win32_Service','99') - $errorMessage = ($script:localizedData.ErrorChangingProperty ` - -f 'Credential',$innerMessage) - $errorRecord = Get-InvalidArgumentRecord ` - -ErrorId 'ChangeCredentialFailed' ` - -ErrorMessage $errorMessage - - It 'Should throw an exception' { - { Write-CredentialProperty ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -DesktopInteract $false ` - } | Should Throw $errorRecord - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-UserNameAndPassword -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 0 - Assert-MockCalled -CommandName Set-LogOnAsServicePolicy -Exactly 0 - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 1 - } - } - - Context 'Both credential and BuiltInAccount passed' { - # Mocks that should not be called - Mock -CommandName Get-UserNameAndPassword - Mock -CommandName Invoke-CimMethod - Mock -CommandName Test-UserName - Mock -CommandName Set-LogOnAsServicePolicy - - $errorRecord = Get-InvalidArgumentRecord ` - -ErrorId 'OnlyCredentialOrBuiltInAccount' ` - -ErrorMessage ($script:localizedData.OnlyOneParameterCanBeSpecified ` - -f 'Credential','BuiltInAccount') - - It 'Should throw an exception' { - { Write-CredentialProperty ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -Credential $script:testCredential ` - -BuiltInAccount 'LocalSystem' } | Should Throw $errorRecord - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-UserNameAndPassword -Exactly 0 - Assert-MockCalled -CommandName Test-UserName -Exactly 0 - Assert-MockCalled -CommandName Set-LogOnAsServicePolicy -Exactly 0 - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 0 - } - } - - Context 'Credential passed but does not need to be changed' { - # Mocks that should be called - Mock -CommandName Get-UserNameAndPassword ` - -MockWith { $script:testUsername,$script:testPassword } ` - -Verifiable - - Mock -CommandName Test-UserName ` - -MockWith { $true } ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName Set-LogOnAsServicePolicy - Mock -CommandName Invoke-CimMethod - - It 'Should not throw an exception' { - { Write-CredentialProperty ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -Credential $script:testCredential } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-UserNameAndPassword -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 1 - Assert-MockCalled -CommandName Set-LogOnAsServicePolicy -Exactly 0 - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 0 - } - } - - Context 'Credential passed and needs to be changed' { - # Mocks that should be called - Mock -CommandName Get-UserNameAndPassword ` - -MockWith { $script:testUsername,$script:testPassword } ` - -Verifiable - - Mock -CommandName Test-UserName ` - -MockWith { $false } ` - -Verifiable - - Mock -CommandName Set-LogOnAsServicePolicy ` - -Verifiable - - Mock -CommandName Invoke-CimMethod ` - -MockWith { @{ returnValue = 0 } } ` - -Verifiable - - It 'Should not throw an exception' { - { Write-CredentialProperty ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -Credential $script:testCredential } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-UserNameAndPassword -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 1 - Assert-MockCalled -CommandName Set-LogOnAsServicePolicy -Exactly 1 - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 1 - } - } - - Context 'Credential passed and needs to be changed, but throws exception' { - # Mocks that should be called - Mock -CommandName Get-UserNameAndPassword ` - -MockWith { $script:testUsername,$script:testPassword } ` - -Verifiable - - Mock -CommandName Test-UserName ` - -MockWith { $false } ` - -Verifiable - - Mock -CommandName Set-LogOnAsServicePolicy ` - -Verifiable - - Mock -CommandName Invoke-CimMethod ` - -MockWith { @{ returnValue = 99 } } ` - -Verifiable - - $innerMessage = ($script:localizedData.MethodFailed ` - -f 'Change','Win32_Service','99') - $errorMessage = ($script:localizedData.ErrorChangingProperty ` - -f 'Credential',$innerMessage) - $errorRecord = Get-InvalidArgumentRecord ` - -ErrorId 'ChangeCredentialFailed' ` - -ErrorMessage $errorMessage - - It 'Should not throw an exception' { - { Write-CredentialProperty ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -Credential $script:testCredential } | Should Throw $errorRecord - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-UserNameAndPassword -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 1 - Assert-MockCalled -CommandName Set-LogOnAsServicePolicy -Exactly 1 - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 1 - } - } - - Context 'BuiltInAccount passed but does not need to be changed' { - # Mocks that should be called - Mock -CommandName Get-UserNameAndPassword ` - -MockWith { '.\LocalSystem','' } ` - -Verifiable - - Mock -CommandName Test-UserName ` - -MockWith { $true } ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName Set-LogOnAsServicePolicy - Mock -CommandName Invoke-CimMethod - - It 'Should not throw an exception' { - { Write-CredentialProperty ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -BuiltInAccount 'LocalSystem' } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-UserNameAndPassword -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 1 - Assert-MockCalled -CommandName Set-LogOnAsServicePolicy -Exactly 0 - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 0 - } - } - - Context 'BuiltInAccount passed and needs to be changed' { - # Mocks that should be called - Mock -CommandName Get-UserNameAndPassword ` - -MockWith { '.\LocalSystem',$null } ` - -Verifiable - - Mock -CommandName Test-UserName ` - -MockWith { $false } ` - -Verifiable - - Mock -CommandName Invoke-CimMethod ` - -MockWith { @{ returnValue = 0 } } ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName Set-LogOnAsServicePolicy - - It 'Should not throw an exception' { - { Write-CredentialProperty ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -BuiltInAccount 'LocalSystem' } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-UserNameAndPassword -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 1 - Assert-MockCalled -CommandName Set-LogOnAsServicePolicy -Exactly 0 - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 1 - } - } - - Context 'BuiltInAccount passed and needs to be changed, but throws exception' { - # Mocks that should be called - Mock -CommandName Get-UserNameAndPassword ` - -MockWith { '.\LocalSystem',$null } ` - -Verifiable - - Mock -CommandName Test-UserName ` - -MockWith { $false } ` - -Verifiable - - Mock -CommandName Invoke-CimMethod ` - -MockWith { @{ returnValue = 99 } } ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName Set-LogOnAsServicePolicy - - $innerMessage = ($script:localizedData.MethodFailed ` - -f 'Change','Win32_Service','99') - $errorMessage = ($script:localizedData.ErrorChangingProperty ` - -f 'Credential',$innerMessage) - $errorRecord = Get-InvalidArgumentRecord ` - -ErrorId 'ChangeCredentialFailed' ` - -ErrorMessage $errorMessage - - It 'Should throw an exception' { - { Write-CredentialProperty ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -BuiltInAccount 'LocalSystem' } | Should Throw $errorRecord - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-UserNameAndPassword -Exactly 1 - Assert-MockCalled -CommandName Test-UserName -Exactly 1 - Assert-MockCalled -CommandName Set-LogOnAsServicePolicy -Exactly 0 - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 1 - } - } - } - - Describe "$script:DscResourceName\Write-BinaryProperty" { - # Stub Functions for Mocking - function Invoke-CimMethod { param ( $InputObject, $MethodName, $Arguments ) } - - Context 'Path is already correct' { - # Mocks that should not be called - Mock -CommandName Invoke-CimMethod - - It 'Should not throw an exception' { - { $script:result = Write-BinaryProperty ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -Path $script:testServiceExecutablePath } | Should Not Throw - } - - It 'Should return false' { - $script:result = $false - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 0 - } - } - - Context 'Path needs to be changed and is changed without error' { - # Mocks that should be called - Mock -CommandName Invoke-CimMethod ` - -MockWith { @{ returnValue = 0 } } ` - -Verifiable - - It 'Should not throw an exception' { - { $script:result = Write-BinaryProperty ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -Path 'c:\NewServicePath.exe' } | Should Not Throw - } - + Describe 'Service\Test-PathsMatch' { + Context 'Specified paths match' { It 'Should return true' { - $script:result = $true - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 1 + $matchingPath = 'MatchingPath' + Test-PathsMatch -ExpectedPath $matchingPath -ActualPath $matchingPath | Should Be $true } } - Context 'Path needs to be changed but an error occurs changing it' { - # Mocks that should be called - Mock -CommandName Invoke-CimMethod ` - -MockWith { @{ returnValue = 99 } } ` - -Verifiable - - $innerMessage = ($script:localizedData.MethodFailed ` - -f 'Change', 'Win32_Service', 99) - $errorMessage = ($script:localizedData.ErrorChangingProperty ` - -f 'Binary Path', $innerMessage) - $errorRecord = Get-InvalidArgumentRecord ` - -ErrorId 'ChangeBinaryPathFailed' ` - -ErrorMessage $errorMessage - - It 'Should throw an exception' { - { $script:result = Write-BinaryProperty ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -Path 'c:\NewServicePath.exe' } | Should Throw $errorRecord - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Invoke-CimMethod -Exactly 1 - } - } - } - - Describe "$script:DscResourceName\Test-UserName" { - Context 'Username matches' { - It 'Should not throw an exception' { - { $script:result = Test-Username ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -Username $script:testUsername } | Should Not Throw - } - - It 'Should return true' { - $script:result = $true - } - } - - Context 'Username does not match' { - It 'Should not throw an exception' { - { $script:result = Test-Username ` - -ServiceWmi $script:testWin32ServiceMockRunningLocalSystem ` - -Username 'mismatch' } | Should Not Throw - } - + Context 'Specified paths do not match' { It 'Should return false' { - $script:result = $false + Test-PathsMatch -ExpectedPath 'Path1' -ActualPath 'Path2' | Should Be $false } } } - Describe "$script:DscResourceName\Get-UserNameAndPassword" { - Context 'Built-in account provided' { - $script:result = Get-UserNameAndPassword -BuiltInAccount 'LocalService' - - It 'Should return: NT Authority\LocalService and $null' { - $script:result[0] | Should Be 'NT Authority\LocalService' - $script:result[1] | Should BeNullOrEmpty - } - } - - Context 'Credential provided' { - $script:result = Get-UserNameAndPassword -Credential $script:testCredential - - It 'Should return the correct username and password' { - $script:result[0] | Should Be ".\$script:testUsername" - $script:result[1] | Should Be $script:testPassword - } - } - - Context 'Neither built-in account or credential provided' { - $script:result = Get-UserNameAndPassword - - It 'Should return both results as null/empty' { - $script:result[0] | Should BeNullOrEmpty - $script:result[1] | Should BeNullOrEmpty - } - } - } - - Describe "$script:DscResourceName\Remove-Service" { - # Mocks that should be called - Mock -CommandName 'sc.exe' -Verifiable - Mock -CommandName Test-ServiceExists -MockWith { $false } -Verifiable - - Context 'Service is deleted successfully' { - # Mocks that should not be called - Mock -CommandName Start-Sleep - - It 'Should not throw exception' { - { - Remove-Service -Name $script:testServiceName - } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName 'sc.exe' -Exactly 1 - Assert-MockCalled -CommandName Test-ServiceExists -Exactly 1 - Assert-MockCalled -CommandName Start-Sleep -Exactly 0 - } - } - - - - Context 'Service can not be deleted (will take 5 seconds)' { - Mock -CommandName Test-ServiceExists -MockWith { $true } -Verifiable - - Mock -CommandName Start-Sleep -Verifiable - - $errorRecord = Get-InvalidArgumentRecord ` - -ErrorId 'ErrorDeletingService' ` - -ErrorMessage ($script:localizedData.ErrorDeletingService -f $script:testServiceName) - - It 'Should throw ErrorDeletingService exception' { - { Remove-Service -Name $script:testServiceName } | Should Throw $errorRecord - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName 'sc.exe' -Exactly 1 - } - } - } - - Describe "$script:DscResourceName\Start-ServiceResource" { - Context 'Service is already running' { - # Mocks that should be called - Mock -CommandName Get-ServiceResource ` - -MockWith { $script:testServiceMockRunning } ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName New-Object - - It 'Should not throw exception' { - { - Start-ServiceResource -Name $script:testServiceName -StartUpTimeout 30000 - } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName New-Object -Exactly 0 - } - } - - Context 'Service is stopped' { - # Mocks that should be called - Mock -CommandName Get-ServiceResource ` - -MockWith { $script:testServiceMockStopped } ` - -Verifiable - - Mock -CommandName New-Object ` - -Verifiable - - $global:ServiceStarted = $false - - It 'Should not throw exception' { - { - Start-ServiceResource -Name $script:testServiceName -StartUpTimeout 30000 - } | Should Not Throw - } - - It 'Should call start method' { - $global:ServiceStarted | Should Be $true - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName New-Object -Exactly 1 - } - - Remove-Variable -Name ServiceStarted -Scope Global - } - } - - Describe "$script:DscResourceName\Stop-ServiceResource" { - Context 'Service is already stopped' { - # Mocks that should be called - Mock -CommandName Get-ServiceResource ` - -MockWith { $script:testServiceMockStopped } ` - -Verifiable - - # Mocks that should not be called - Mock -CommandName New-Object - - It 'Should not throw exception' { - { - Stop-ServiceResource -Name $script:testServiceName -TerminateTimeout 30000 - } | Should Not Throw - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName New-Object -Exactly 0 - } - } - - Context 'Service is running' { - # Mocks that should be called - Mock -CommandName Get-ServiceResource ` - -MockWith { $script:testServiceMockRunning } ` - -Verifiable - - Mock -CommandName New-Object ` - -Verifiable - - $global:ServiceStopped = $false - - It 'Should not throw exception' { - { - Stop-ServiceResource -Name $script:testServiceName -TerminateTimeout 30000 - } | Should Not Throw - } - - It 'Should call stop method' { - $global:ServiceStopped | Should Be $true - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled -CommandName Get-ServiceResource -Exactly 1 - Assert-MockCalled -CommandName New-Object -Exactly 1 - } - - Remove-Variable -Name ServiceStopped -Scope Global - } - } - - Describe "$script:DscResourceName\Resolve-UserName" { - Context 'Username is NetworkService' { - It 'Should return NT Authority\NetworkService' { - Resolve-UserName -Username 'NetworkService' | Should Be 'NT Authority\NetworkService' - } - } - - Context 'Username is LocalService' { - It 'Should return NT Authority\LocalService' { - Resolve-UserName -Username 'LocalService' | Should Be 'NT Authority\LocalService' - } - } - - Context 'Username is LocalSystem' { + Describe 'Service\ConvertTo-StartName' { + Context 'Username specified as LocalSystem' { It 'Should return .\LocalSystem' { - Resolve-UserName -Username 'LocalSystem' | Should Be '.\LocalSystem' + ConvertTo-StartName -Username 'LocalSystem' | Should Be '.\LocalSystem' } } - Context 'Username is Domain\svcAccount' { - It 'Should return Domain\svcAccount' { - Resolve-UserName -Username 'Domain\svcAccount' | Should Be 'Domain\svcAccount' + Context 'Username specified as LocalService' { + It 'Should return NT Authority\LocalService' { + ConvertTo-StartName -Username 'LocalService' | Should Be 'NT Authority\LocalService' } } - Context 'Username is svcAccount' { - It 'Should return .\svcAccount' { - Resolve-UserName -Username 'svcAccount' | Should Be '.\svcAccount' + Context 'Username specified as NetworkService' { + It 'Should return NT Authority\NetworkService' { + ConvertTo-StartName -Username 'NetworkService' | Should Be 'NT Authority\NetworkService' + } + } + + Context 'Custom username specified without any \ or @ characters' { + It 'Should return custom username prefixed with .\' { + $customUsername = 'TestUsername' + ConvertTo-StartName -Username $customUsername | Should Be ".\$customUsername" + } + } + + Context 'Custom username specified that starts with the local computer name followed by a \ character' { + It 'Should return custom username prefixed with .\ instead of the local computer name' { + $customUsername = 'TestUsername' + $customUsernameWithComputerNamePrefix = "$env:computerName\$customUsername" + ConvertTo-StartName -Username $customUsernameWithComputerNamePrefix | Should Be ".\$customUsername" + } + } + + Context 'Custom username specified with a \ character and a custom domain' { + It 'Should return the custom username with no changes' { + $customUsername = 'TestDomain\TestUsername' + ConvertTo-StartName -Username $customUsername | Should Be $customUsername + } + } + + Context 'Custom username specified with an @ character' { + It 'Should return the custom username with no changes' { + $customUsername = 'TestUsername@TestDomain' + ConvertTo-StartName -Username $customUsername | Should Be $customUsername } } } - Describe "$script:DscResourceName\Test-ServiceExists" { - Context 'Service exists' { - # Mocks that should be called - Mock -CommandName Get-Service ` - -ParameterFilter { $Name -eq $script:testServiceName } ` - -MockWith { $script:testServiceMockRunning } ` - -Verifiable + Describe 'Service\Set-ServicePath' { + $testServiceCimInstance = New-CimInstance -ClassName 'Win32_Service' -Property @{ PathName = 'TestPath' } -ClientOnly - It 'Should not throw an exception' { - { - $script:result = Test-ServiceExists -Name $script:testServiceName - } | Should Not Throw + try + { + Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } + Mock -CommandName 'Test-PathsMatch' -MockWith { return $true } + + $invokeCimMethodSuccessResult = @{ + ReturnValue = 0 } - It 'Should return true' { - $script:Result | Should Be $true + Mock -CommandName 'Invoke-CimMethod' -MockWith { return $invokeCimMethodSuccessResult } + + Context 'Specified path matches the service path' { + $setServicePathParameters = @{ + ServiceName = $script:testServiceName + Path = $testServiceCimInstance.PathName + } + + It 'Should not throw' { + { Set-ServicePath @setServicePathParameters } | Should Not Throw + } + + It 'Should retrieve the service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServicePathParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should test if the specfied path matches the service path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -ParameterFilter { $ExpectedPath -eq $setServicePathParameters.Path -and $ActualPath -eq $testServiceCimInstance.PathName } -Times 1 -Scope 'Context' + } + + It 'Should not change the service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -Times 0 -Scope 'Context' + } + + It 'Should return false' { + Set-ServicePath @setServicePathParameters | Should Be $false + } } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled ` - -CommandName Get-Service ` - -ParameterFilter { $Name -eq $script:testServiceName } ` - -Exactly 1 + Mock -CommandName 'Test-PathsMatch' -MockWith { return $false } + + Context 'Specified path does not match the service path and the path change succeeds' { + $setServicePathParameters = @{ + ServiceName = $script:testServiceName + Path = 'NewTestPath' + } + + It 'Should not throw' { + { Set-ServicePath @setServicePathParameters } | Should Not Throw + } + + It 'Should retrieve the service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServicePathParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should test if the specfied path matches the service path' { + Assert-MockCalled -CommandName 'Test-PathsMatch' -ParameterFilter { $ExpectedPath -eq $setServicePathParameters.Path -and $ActualPath -eq $testServiceCimInstance.PathName } -Times 1 -Scope 'Context' + } + + It 'Should change the service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -ParameterFilter { $InputObject -eq $testServiceCimInstance -and $MethodName -eq 'Change' -and $Arguments.PathName -eq $setServicePathParameters.Path} -Times 1 -Scope 'Context' + } + + It 'Should return true' { + Set-ServicePath @setServicePathParameters | Should Be $true + } + } + + $invokeCimMethodFailResult = @{ + ReturnValue = 1 + } + + Mock -CommandName 'Invoke-CimMethod' -MockWith { return $invokeCimMethodFailResult } + + Context 'Specified path does not match the service path and the path change fails' { + $setServicePathParameters = @{ + ServiceName = $script:testServiceName + Path = 'NewTestPath' + } + + It 'Should throw error for failed service path change' { + $errorMessage = $script:localizedData.InvokeCimMethodFailed -f 'Change', $setServicePathParameters.ServiceName, 'PathName', $invokeCimMethodFailResult.ReturnValue + + { Set-ServicePath @setServicePathParameters } | Should Throw $errorMessage + } + } + } + finally + { + $testServiceCimInstance.Dispose() + + # Release the reference so the garbage collector can clean up + $testServiceCimInstance = $null + } + } + + Describe 'Service\Set-ServiceDependencies' { + $testServiceCimInstance = New-CimInstance -ClassName 'Win32_Service' -ClientOnly + + try { + $testService = @{ + ServicesDependedOn = @( @{ Name = 'TestDependency1' }, @{ Name = 'TestDependency2'} ) + } + + Mock -CommandName 'Get-Service' -MockWith { return $testService } + Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } + + $invokeCimMethodSuccessResult = @{ + ReturnValue = 0 + } + + Mock -CommandName 'Invoke-CimMethod' -MockWith { return $invokeCimMethodSuccessResult } + + Context 'Specified dependencies match the service dependencies' { + $setServiceDependenciesParameters = @{ + ServiceName = $script:testServiceName + Dependencies = $testService.ServicesDependedOn.Name + } + + It 'Should not throw' { + { Set-ServiceDependencies @setServiceDependenciesParameters } | Should Not Throw + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setServiceDependenciesParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should not retrieve the service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -Times 0 -Scope 'Context' + } + + It 'Should not change the service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -Times 0 -Scope 'Context' + } + } + + Context 'Specified dependencies do not match the populated service dependencies and the dependency change succeeds' { + $setServiceDependenciesParameters = @{ + ServiceName = $script:testServiceName + Dependencies = @( 'TestDependency3', 'TestDependency4' ) + } + + It 'Should not throw' { + { Set-ServiceDependencies @setServiceDependenciesParameters } | Should Not Throw + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setServiceDependenciesParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should retrieve the service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServiceDependenciesParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should change the service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -ParameterFilter { $InputObject -eq $testServiceCimInstance -and $MethodName -eq 'Change' -and $null -eq (Compare-Object -ReferenceObject $setServiceDependenciesParameters.Dependencies -DifferenceObject $Arguments.ServiceDependencies) } -Times 1 -Scope 'Context' + } + } + + Context 'Specified empty dependencies do not match the populated service dependencies and the dependency change succeeds' { + $setServiceDependenciesParameters = @{ + ServiceName = $script:testServiceName + Dependencies = @() + } + + It 'Should not throw' { + { Set-ServiceDependencies @setServiceDependenciesParameters } | Should Not Throw + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setServiceDependenciesParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should retrieve the service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServiceDependenciesParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should change the service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -ParameterFilter { $InputObject -eq $testServiceCimInstance -and $MethodName -eq 'Change' -and $null -eq (Compare-Object -ReferenceObject $setServiceDependenciesParameters.Dependencies -DifferenceObject $Arguments.ServiceDependencies) } -Times 1 -Scope 'Context' + } + } + + $testServiceWithNoDependencies = @{ + ServicesDependedOn = $null + } + + Mock -CommandName 'Get-Service' -MockWith { return $testServiceWithNoDependencies } + + Context 'Specified empty dependencies match the null service dependencies' { + $setServiceDependenciesParameters = @{ + ServiceName = $script:testServiceName + Dependencies = @() + } + + It 'Should not throw' { + { Set-ServiceDependencies @setServiceDependenciesParameters } | Should Not Throw + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setServiceDependenciesParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should not retrieve the service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -Times 0 -Scope 'Context' + } + + It 'Should not change the service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -Times 0 -Scope 'Context' + } + } + + Context 'Specified dependencies do not match the null service dependencies and the dependency change succeeds' { + $setServiceDependenciesParameters = @{ + ServiceName = $script:testServiceName + Dependencies = @( 'TestDependency3', 'TestDependency4' ) + } + + It 'Should not throw' { + { Set-ServiceDependencies @setServiceDependenciesParameters } | Should Not Throw + } + + It 'Should retrieve the service' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $setServiceDependenciesParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should retrieve the service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServiceDependenciesParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should change the service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -ParameterFilter { $InputObject -eq $testServiceCimInstance -and $MethodName -eq 'Change' -and $null -eq (Compare-Object -ReferenceObject $setServiceDependenciesParameters.Dependencies -DifferenceObject $Arguments.ServiceDependencies) } -Times 1 -Scope 'Context' + } + } + + $invokeCimMethodFailResult = @{ + ReturnValue = 1 + } + + Mock -CommandName 'Invoke-CimMethod' -MockWith { return $invokeCimMethodFailResult } + + Context 'Specified dependencies do not match the service dependencies and the dependency change fails' { + $setServiceDependenciesParameters = @{ + ServiceName = $script:testServiceName + Dependencies = @( 'TestDependency3', 'TestDependency4' ) + } + + It 'Should throw error for failed service path change' { + $errorMessage = $script:localizedData.InvokeCimMethodFailed -f 'Change', $setServiceDependenciesParameters.ServiceName, 'ServiceDependencies', $invokeCimMethodFailResult.ReturnValue + + { Set-ServiceDependencies @setServiceDependenciesParameters } | Should Throw $errorMessage + } + } + } + finally + { + $testServiceCimInstance.Dispose() + } + } + + Describe 'Service\Set-ServiceAccountProperty' { + $testServiceCimInstance = New-CimInstance -ClassName 'Win32_Service' -Property @{ StartName = 'LocalSystem'; DesktopInteract = $true } -ClientOnly + + try { + Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } + Mock -CommandName 'Grant-LogOnAsServiceRight' -MockWith { } + Mock -CommandName 'ConvertTo-StartName' -MockWith { return $Username } + + $invokeCimMethodSuccessResult = @{ + ReturnValue = 0 + } + + Mock -CommandName 'Invoke-CimMethod' -MockWith { return $invokeCimMethodSuccessResult } + + Context 'No parameters specified' { + $setServiceAccountPropertyParameters = @{ + ServiceName = $script:testServiceName + } + + It 'Should not throw' { + { Set-ServiceAccountProperty @setServiceAccountPropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServiceAccountPropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to convert the built-in account or credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to grant Log on As a Service right' { + Assert-MockCalled -CommandName 'Grant-LogOnAsServiceRight' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -Times 0 -Scope 'Context' + } + } + + Context 'Matching DesktopInteract specified' { + $setServiceAccountPropertyParameters = @{ + ServiceName = $script:testServiceName + DesktopInteract = $testServiceCimInstance.DesktopInteract + } + + It 'Should not throw' { + { Set-ServiceAccountProperty @setServiceAccountPropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServiceAccountPropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to convert the built-in account or credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to grant Log on As a Service right' { + Assert-MockCalled -CommandName 'Grant-LogOnAsServiceRight' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -Times 0 -Scope 'Context' + } + } + + Context 'Mismatching DesktopInteract specified and service change succeeds' { + $setServiceAccountPropertyParameters = @{ + ServiceName = $script:testServiceName + DesktopInteract = -not $testServiceCimInstance.DesktopInteract + } + + It 'Should not throw' { + { Set-ServiceAccountProperty @setServiceAccountPropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServiceAccountPropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to convert the built-in account or credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to grant Log on As a Service right' { + Assert-MockCalled -CommandName 'Grant-LogOnAsServiceRight' -Times 0 -Scope 'Context' + } + + It 'Should change service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -ParameterFilter { $InputObject -eq $testServiceCimInstance -and $MethodName -eq 'Change' -and $Arguments.DesktopInteract -eq $setServiceAccountPropertyParameters.DesktopInteract} -Times 1 -Scope 'Context' + } + } + + Context 'Credential with matching username specified' { + $secureTestPassword = ConvertTo-SecureString -String 'TestPassword' -AsPlainText -Force + $testCredentialWithMatchingUsername = New-Object -TypeName 'PSCredential' -ArgumentList @( $testServiceCimInstance.StartName, $secureTestPassword ) + + $setServiceAccountPropertyParameters = @{ + ServiceName = $script:testServiceName + Credential = $testCredentialWithMatchingUsername + } + + It 'Should not throw' { + { Set-ServiceAccountProperty @setServiceAccountPropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServiceAccountPropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should convert the credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -ParameterFilter { $Username -eq $setServiceAccountPropertyParameters.Credential.UserName } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to grant Log on As a Service right' { + Assert-MockCalled -CommandName 'Grant-LogOnAsServiceRight' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -Times 0 -Scope 'Context' + } + } + + Context 'Credential with mismatching username specified and service change succeeds' { + $setServiceAccountPropertyParameters = @{ + ServiceName = $script:testServiceName + Credential = $script:testCredential1 + } + + It 'Should not throw' { + { Set-ServiceAccountProperty @setServiceAccountPropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServiceAccountPropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should convert the credential username to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -ParameterFilter { $Username -eq $setServiceAccountPropertyParameters.Credential.UserName } -Times 1 -Scope 'Context' + } + + It 'Should grant Log on As a Service right' { + Assert-MockCalled -CommandName 'Grant-LogOnAsServiceRight' -ParameterFilter { $Username -eq $setServiceAccountPropertyParameters.Credential.UserName } -Times 1 -Scope 'Context' + } + + It 'Should change service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -ParameterFilter { $InputObject -eq $testServiceCimInstance -and $MethodName -eq 'Change' -and $Arguments.StartName -eq $setServiceAccountPropertyParameters.Credential.UserName -and $Arguments.StartPassword -eq $setServiceAccountPropertyParameters.Credential.GetNetworkCredential().Password } -Times 1 -Scope 'Context' + } + } + + Context 'Matching BuiltInAccount specified' { + $setServiceAccountPropertyParameters = @{ + ServiceName = $script:testServiceName + BuiltInAccount = $testServiceCimInstance.StartName + } + + It 'Should not throw' { + { Set-ServiceAccountProperty @setServiceAccountPropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServiceAccountPropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should convert the built-in account to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -ParameterFilter { $Username -eq $setServiceAccountPropertyParameters.BuiltInAccount } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to grant Log on As a Service right' { + Assert-MockCalled -CommandName 'Grant-LogOnAsServiceRight' -Times 0 -Scope 'Context' + } + + It 'Should not attempt to change service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -Times 0 -Scope 'Context' + } + } + + Context 'Mismatching BuiltInAccount specified and service change succeeds' { + $setServiceAccountPropertyParameters = @{ + ServiceName = $script:testServiceName + BuiltInAccount = 'NetworkService' + } + + It 'Should not throw' { + { Set-ServiceAccountProperty @setServiceAccountPropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServiceAccountPropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should convert the built-in account to a service start name' { + Assert-MockCalled -CommandName 'ConvertTo-StartName' -ParameterFilter { $Username -eq $setServiceAccountPropertyParameters.BuiltInAccount } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to grant Log on As a Service right' { + Assert-MockCalled -CommandName 'Grant-LogOnAsServiceRight' -Times 0 -Scope 'Context' + } + + It 'Should change service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -ParameterFilter { $InputObject -eq $testServiceCimInstance -and $MethodName -eq 'Change' -and $Arguments.StartName -eq $setServiceAccountPropertyParameters.BuiltInAccount -and $Arguments.StartPassword -eq [String]::Empty } -Times 1 -Scope 'Context' + } + } + + $invokeCimMethodFailResult = @{ + ReturnValue = 1 + } + + Mock -CommandName 'Invoke-CimMethod' -MockWith { return $invokeCimMethodFailResult } + + Context 'Mismatching DesktopInteract specified and service change fails' { + $setServiceAccountPropertyParameters = @{ + ServiceName = $script:testServiceName + DesktopInteract = -not $testServiceCimInstance.DesktopInteract + } + + It 'Should throw an error for service change failure' { + $errorMessage = $script:localizedData.InvokeCimMethodFailed -f 'Change', $setServiceAccountPropertyParameters.ServiceName, 'DesktopInteract', $invokeCimMethodFailResult.ReturnValue + + { Set-ServiceAccountProperty @setServiceAccountPropertyParameters } | Should Throw $errorMessage + } + } + + Context 'Credential with mismatching username specified and service change fails' { + $setServiceAccountPropertyParameters = @{ + ServiceName = $script:testServiceName + Credential = $script:testCredential1 + } + + It 'Should throw an error for service change failure' { + $errorMessage = $script:localizedData.InvokeCimMethodFailed -f 'Change', $setServiceAccountPropertyParameters.ServiceName, 'StartName, StartPassword', $invokeCimMethodFailResult.ReturnValue + + { Set-ServiceAccountProperty @setServiceAccountPropertyParameters } | Should Throw $errorMessage + } + } + + Context 'Mismatching BuiltInAccount specified and service change fails' { + $setServiceAccountPropertyParameters = @{ + ServiceName = $script:testServiceName + BuiltInAccount = 'NetworkService' + } + + It 'Should throw an error for service change failure' { + $errorMessage = $script:localizedData.InvokeCimMethodFailed -f 'Change', $setServiceAccountPropertyParameters.ServiceName, 'StartName, StartPassword', $invokeCimMethodFailResult.ReturnValue + + { Set-ServiceAccountProperty @setServiceAccountPropertyParameters } | Should Throw $errorMessage + } + } + } + finally + { + $testServiceCimInstance.Dispose() + + # Release the reference so the garbage collector can clean up + $testServiceCimInstance = $null + } + } + + Describe 'Service\Set-ServiceStartupType' { + $testServiceCimInstance = New-CimInstance -ClassName 'Win32_Service' -Property @{ StartMode = 'Manual' } -ClientOnly + + try { + Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } + Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $testServiceCimInstance.StartMode } + + $invokeCimMethodSuccessResult = @{ + ReturnValue = 0 + } + + Mock -CommandName 'Invoke-CimMethod' -MockWith { return $invokeCimMethodSuccessResult } + + Context 'Specified startup type matches the service startup type' { + $setServiceStartupTypeParameters = @{ + ServiceName = $script:testServiceName + StartupType = $testServiceCimInstance.StartMode + } + + It 'Should not throw' { + { Set-ServiceStartupType @setServiceStartupTypeParameters } | Should Not Throw + } + + It 'Should retrieve the service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServiceStartupTypeParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should not attempt to change the service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -Times 0 -Scope 'Context' + } + } + + Context 'Specified startup type does not match the service startup type and service change succeeds' { + $setServiceStartupTypeParameters = @{ + ServiceName = $script:testServiceName + StartupType = 'Automatic' + } + + It 'Should not throw' { + { Set-ServiceStartupType @setServiceStartupTypeParameters } | Should Not Throw + } + + It 'Should retrieve the service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServiceStartupTypeParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should change the service' { + Assert-MockCalled -CommandName 'Invoke-CimMethod' -ParameterFilter { $InputObject -eq $testServiceCimInstance -and $MethodName -eq 'Change' -and $Arguments.StartMode -eq $setServiceStartupTypeParameters.StartupType } -Times 1 -Scope 'Context' + } + } + + $invokeCimMethodFailResult = @{ + ReturnValue = 1 + } + + Mock -CommandName 'Invoke-CimMethod' -MockWith { return $invokeCimMethodFailResult } + + Context 'Specified startup type does not match the service startup type and service change fails' { + $setServiceStartupTypeParameters = @{ + ServiceName = $script:testServiceName + StartupType = 'Automatic' + } + + It 'Should throw error for failed service change' { + $errorMessage = $script:localizedData.InvokeCimMethodFailed -f 'Change', $setServiceStartupTypeParameters.ServiceName, 'StartMode', $invokeCimMethodFailResult.ReturnValue + + { Set-ServiceStartupType @setServiceStartupTypeParameters } | Should Throw $errorMessage + } + } + } + finally + { + $testServiceCimInstance.Dispose() + + # Release the reference so the garbage collector can clean up + $testServiceCimInstance = $null + } + } + + Describe 'Service\Set-ServiceProperty' { + $testServiceCimInstance = @{ + Description = 'Test service description' + DisplayName = 'TestDisplayName' + } + + Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } + Mock -CommandName 'Set-Service' -MockWith { } + Mock -CommandName 'Set-ServiceDependencies' -MockWith { } + Mock -CommandName 'Set-ServiceAccountProperty' -MockWith { } + Mock -CommandName 'Set-ServiceStartupType' -MockWith { } + + Context 'No parameters specified' { + $setServicePropertyParameters = @{ + ServiceName = $script:testServiceName + } + + It 'Should not throw' { + { Set-ServiceProperty @setServicePropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServicePropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should not set service description or display name' { + Assert-MockCalled -CommandName 'Set-Service' -Times 0 -Scope 'Context' + } + + It 'Should not set service dependencies' { + Assert-MockCalled -CommandName 'Set-ServiceDependencies' -Times 0 -Scope 'Context' + } + + It 'Should not set service account properties' { + Assert-MockCalled -CommandName 'Set-ServiceAccountProperty' -Times 0 -Scope 'Context' + } + + It 'Should not set service startup type' { + Assert-MockCalled -CommandName 'Set-ServiceStartupType' -Times 0 -Scope 'Context' } } - Context 'Service does not exist' { - # Mocks that should be called - Mock -CommandName Get-Service ` - -ParameterFilter { $Name -eq $script:testServiceName } ` - -Verifiable - - It 'Should not throw an exception' { - { - $script:result = Test-ServiceExists -Name $script:testServiceName - } | Should Not Throw + Context 'Mismatching DisplayName specified' { + $setServicePropertyParameters = @{ + ServiceName = $script:testServiceName + DisplayName = 'NewDisplayName' } - It 'Should return false' { - $script:Result | Should Be $false + It 'Should not throw' { + { Set-ServiceProperty @setServicePropertyParameters } | Should Not Throw } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled ` - -CommandName Get-Service ` - -ParameterFilter { $Name -eq $script:testServiceName } ` - -Exactly 1 + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServicePropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should set service display name' { + Assert-MockCalled -CommandName 'Set-Service' -ParameterFilter { $Name -eq $setServicePropertyParameters.ServiceName -and $DisplayName -eq $setServicePropertyParameters.DisplayName } -Times 1 -Scope 'Context' + } + + It 'Should not set service dependencies' { + Assert-MockCalled -CommandName 'Set-ServiceDependencies' -Times 0 -Scope 'Context' + } + + It 'Should not set service account properties' { + Assert-MockCalled -CommandName 'Set-ServiceAccountProperty' -Times 0 -Scope 'Context' + } + + It 'Should not set service startup type' { + Assert-MockCalled -CommandName 'Set-ServiceStartupType' -Times 0 -Scope 'Context' + } + } + + Context 'Mismatching Description specified' { + $setServicePropertyParameters = @{ + ServiceName = $script:testServiceName + Description = 'New service description' + } + + It 'Should not throw' { + { Set-ServiceProperty @setServicePropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServicePropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should set service description' { + Assert-MockCalled -CommandName 'Set-Service' -ParameterFilter { $Name -eq $setServicePropertyParameters.ServiceName -and $Description -eq $setServicePropertyParameters.Description } -Times 1 -Scope 'Context' + } + + It 'Should not set service dependencies' { + Assert-MockCalled -CommandName 'Set-ServiceDependencies' -Times 0 -Scope 'Context' + } + + It 'Should not set service account properties' { + Assert-MockCalled -CommandName 'Set-ServiceAccountProperty' -Times 0 -Scope 'Context' + } + + It 'Should not set service startup type' { + Assert-MockCalled -CommandName 'Set-ServiceStartupType' -Times 0 -Scope 'Context' + } + } + + Context 'Matching Description and DisplayName specified' { + $setServicePropertyParameters = @{ + ServiceName = $script:testServiceName + DisplayName = $testServiceCimInstance.DisplayName + Description = $testServiceCimInstance.Description + } + + It 'Should not throw' { + { Set-ServiceProperty @setServicePropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServicePropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should not set service description or display name' { + Assert-MockCalled -CommandName 'Set-Service' -Times 0 -Scope 'Context' + } + + It 'Should not set service dependencies' { + Assert-MockCalled -CommandName 'Set-ServiceDependencies' -Times 0 -Scope 'Context' + } + + It 'Should not set service account properties' { + Assert-MockCalled -CommandName 'Set-ServiceAccountProperty' -Times 0 -Scope 'Context' + } + + It 'Should not set service startup type' { + Assert-MockCalled -CommandName 'Set-ServiceStartupType' -Times 0 -Scope 'Context' + } + } + + Context 'Dependencies specified' { + $setServicePropertyParameters = @{ + ServiceName = $script:testServiceName + Dependencies = @( 'TestDependency1' ) + } + + It 'Should not throw' { + { Set-ServiceProperty @setServicePropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServicePropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should not set service description or display name' { + Assert-MockCalled -CommandName 'Set-Service' -Times 0 -Scope 'Context' + } + + It 'Should set service dependencies' { + Assert-MockCalled -CommandName 'Set-ServiceDependencies' -ParameterFilter { $ServiceName -eq $setServicePropertyParameters.ServiceName -and $null -eq (Compare-Object -ReferenceObject $setServicePropertyParameters.Dependencies -DifferenceObject $Dependencies) } -Times 1 -Scope 'Context' + } + + It 'Should not set service account properties' { + Assert-MockCalled -CommandName 'Set-ServiceAccountProperty' -Times 0 -Scope 'Context' + } + + It 'Should not set service startup type' { + Assert-MockCalled -CommandName 'Set-ServiceStartupType' -Times 0 -Scope 'Context' + } + } + + Context 'Credential specified' { + $setServicePropertyParameters = @{ + ServiceName = $script:testServiceName + Credential = $script:testCredential1 + } + + It 'Should not throw' { + { Set-ServiceProperty @setServicePropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServicePropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should not set service description or display name' { + Assert-MockCalled -CommandName 'Set-Service' -Times 0 -Scope 'Context' + } + + It 'Should not set service dependencies' { + Assert-MockCalled -CommandName 'Set-ServiceDependencies' -Times 0 -Scope 'Context' + } + + It 'Should set service account properties' { + Assert-MockCalled -CommandName 'Set-ServiceAccountProperty' -ParameterFilter { $ServiceName -eq $setServicePropertyParameters.ServiceName -and [PSCredential]::Equals($setServicePropertyParameters.Credential, $Credential) } -Times 1 -Scope 'Context' + } + + It 'Should not set service startup type' { + Assert-MockCalled -CommandName 'Set-ServiceStartupType' -Times 0 -Scope 'Context' + } + } + + Context 'BuiltInAccount specified' { + $setServicePropertyParameters = @{ + ServiceName = $script:testServiceName + BuiltInAccount = 'LocalService' + } + + It 'Should not throw' { + { Set-ServiceProperty @setServicePropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServicePropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should not set service description or display name' { + Assert-MockCalled -CommandName 'Set-Service' -Times 0 -Scope 'Context' + } + + It 'Should not set service dependencies' { + Assert-MockCalled -CommandName 'Set-ServiceDependencies' -Times 0 -Scope 'Context' + } + + It 'Should set service account properties' { + Assert-MockCalled -CommandName 'Set-ServiceAccountProperty' -ParameterFilter { $ServiceName -eq $setServicePropertyParameters.ServiceName -and $BuiltInAccount -eq $setServicePropertyParameters.BuiltInAccount } -Times 1 -Scope 'Context' + } + + It 'Should not set service startup type' { + Assert-MockCalled -CommandName 'Set-ServiceStartupType' -Times 0 -Scope 'Context' + } + } + + Context 'DesktopInteract specified' { + $setServicePropertyParameters = @{ + ServiceName = $script:testServiceName + DesktopInteract = $true + } + + It 'Should not throw' { + { Set-ServiceProperty @setServicePropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServicePropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should not set service description or display name' { + Assert-MockCalled -CommandName 'Set-Service' -Times 0 -Scope 'Context' + } + + It 'Should not set service dependencies' { + Assert-MockCalled -CommandName 'Set-ServiceDependencies' -Times 0 -Scope 'Context' + } + + It 'Should set service account properties' { + Assert-MockCalled -CommandName 'Set-ServiceAccountProperty' -ParameterFilter { $ServiceName -eq $setServicePropertyParameters.ServiceName -and $DesktopInteract -eq $setServicePropertyParameters.DesktopInteract } -Times 1 -Scope 'Context' + } + + It 'Should not set service startup type' { + Assert-MockCalled -CommandName 'Set-ServiceStartupType' -Times 0 -Scope 'Context' + } + } + + Context 'StartupType specified' { + $setServicePropertyParameters = @{ + ServiceName = $script:testServiceName + StartupType = 'Manual' + } + + It 'Should not throw' { + { Set-ServiceProperty @setServicePropertyParameters } | Should Not Throw + } + + It 'Should retrieve service CIM instance' { + Assert-MockCalled -CommandName 'Get-ServiceCimInstance' -ParameterFilter { $ServiceName -eq $setServicePropertyParameters.ServiceName } -Times 1 -Scope 'Context' + } + + It 'Should not set service description or display name' { + Assert-MockCalled -CommandName 'Set-Service' -Times 0 -Scope 'Context' + } + + It 'Should not set service dependencies' { + Assert-MockCalled -CommandName 'Set-ServiceDependencies' -Times 0 -Scope 'Context' + } + + It 'Should not set service account properties' { + Assert-MockCalled -CommandName 'Set-ServiceAccountProperty' -Times 0 -Scope 'Context' + } + + It 'Should set service startup type' { + Assert-MockCalled -CommandName 'Set-ServiceStartupType' -ParameterFilter { $ServiceName -eq $setServicePropertyParameters.ServiceName -and $StartupType -eq $setServicePropertyParameters.StartupType } -Times 1 -Scope 'Context' } } } - Describe "$script:DscResourceName\Compare-ServicePath" { - Context 'Service exists, path matches' { - # Mocks that should be called - Mock -CommandName Get-CimInstance ` - -MockWith { $script:testWin32ServiceMockRunningLocalSystem } ` - -Verifiable + Describe 'Service\Remove-ServiceWithTimeout' { + Mock -CommandName 'Remove-Service' -MockWith { } + Mock -CommandName 'Get-Service' -MockWith { } - It 'Should not throw an exception' { - { $script:result = Compare-ServicePath ` - -Name $script:testServiceName ` - -Path $script:testServiceExecutablePath ` - } | Should Not Throw + Context 'Service removal succeeds' { + $removeServiceWithTimeoutParameters = @{ + Name = $script:testServiceName + TerminateTimeout = 500 } - It 'Should return true' { - $script:Result | Should Be $true + It 'Should not throw' { + { Remove-ServiceWithTimeout @removeServiceWithTimeoutParameters } | Should Not Throw } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled ` - -CommandName Get-CimInstance ` - -Exactly 1 + It 'Should remove service' { + Assert-MockCalled -CommandName 'Remove-Service' -ParameterFilter { $Name -eq $removeServiceWithTimeoutParameters.Name } -Times 1 -Scope 'Context' + } + + It 'Should retrieve service to check for removal once' { + Assert-MockCalled -CommandName 'Get-Service' -ParameterFilter { $Name -eq $removeServiceWithTimeoutParameters.Name } -Times 1 -Scope 'Context' } } - Context 'Service exists, path does not match' { - # Mocks that should be called - Mock -CommandName Get-CimInstance ` - -MockWith { $script:testWin32ServiceMockRunningLocalSystem } ` - -Verifiable + Mock -CommandName 'Get-Service' -MockWith { return 'Not null' } - It 'Should not throw an exception' { - { $script:result = Compare-ServicePath ` - -Name $script:testServiceName ` - -Path 'c:\differentpath' ` - } | Should Not Throw + Context 'Service removal fails' { + $removeServiceWithTimeoutParameters = @{ + Name = $script:testServiceName + TerminateTimeout = 500 } - It 'Should return false' { - $script:Result | Should Be $false - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled ` - -CommandName Get-CimInstance ` - -Exactly 1 - } - } - - Context 'Service does not exist' { - # Mocks that should be called - Mock -CommandName Get-CimInstance ` - -Verifiable - - It 'Should not throw an exception' { - { $script:result = Compare-ServicePath ` - -Name $script:testServiceName ` - -Path 'c:\differentpath' ` - } | Should Not Throw - } - - It 'Should return false' { - $script:Result | Should Be $false - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled ` - -CommandName Get-CimInstance ` - -Exactly 1 + It 'Should throw error for service removal timeout' { + $errorMessage = $script:localizedData.ServiceDeletionFailed -f $removeServiceWithTimeoutParameters.Name + { Remove-ServiceWithTimeout @removeServiceWithTimeoutParameters } | Should Throw $errorMessage } } } - Describe "$script:DscResourceName\Get-ServiceResource" { - Context 'Service exists' { - # Mocks that should be called - Mock -CommandName Get-Service ` - -ParameterFilter { $Name -eq $script:testServiceName } ` - -MockWith { $script:testServiceMockRunning } ` - -Verifiable + Describe 'Service\Start-ServiceWithTimeout' { + Mock -CommandName 'Start-Service' -MockWith { } + Mock -CommandName 'Wait-ServiceStateWithTimeout' -MockWith { } - It 'Should not throw an exception' { - { - $script:service = Get-ServiceResource -Name $script:testServiceName - } | Should Not Throw - } - - It 'Should return the correct hashtable properties' { - $script:service.Name | Should Be $script:testServiceName - $script:service.ServiceName | Should Be $script:testServiceName - $script:service.DisplayName | Should Be $script:testServiceDisplayName - $script:service.StartType | Should Be $script:testServiceStartupType - $script:service.Status | Should Be $script:testServiceStatusRunning - $script:service.ServicesDependedOn | Should Be $script:testServiceDependsOnHash - } - - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled ` - -CommandName Get-Service ` - -ParameterFilter { $Name -eq $script:testServiceName } ` - -Exactly 1 - } + $startServiceWithTimeoutParameters = @{ + ServiceName = $script:testServiceName + StartupTimeout = 500 } - Context 'Service does not exist' { - # Mocks that should be called - Mock -CommandName Get-Service ` - -ParameterFilter { $Name -eq $script:testServiceName } ` - -Verifiable + $expectedTimeSpan = [TimeSpan]::FromMilliseconds($startServiceWithTimeoutParameters.StartupTimeout) + + It 'Should not throw' { + { Start-ServiceWithTimeout @startServiceWithTimeoutParameters } | Should Not Throw + } - $errorRecord = Get-InvalidArgumentRecord ` - -ErrorId 'ServiceNotFound' ` - -ErrorMessage ($script:localizedData.ServiceNotFound -f $script:testServiceName) + It 'Should start service' { + Assert-MockCalled -CommandName 'Start-Service' -ParameterFilter { $Name -eq $startServiceWithTimeoutParameters.ServiceName } -Times 1 -Scope 'Describe' + } - It 'Should throw a ServiceNotFound exception' { - { - $script:service = Get-ServiceResource -Name $script:testServiceName - } | Should Throw $errorRecord - } + It 'Should wait for service to start' { + Assert-MockCalled -CommandName 'Wait-ServiceStateWithTimeout' -ParameterFilter { $ServiceName -eq $startServiceWithTimeoutParameters.ServiceName -and $State -eq [System.ServiceProcess.ServiceControllerStatus]::Running -and [TimeSpan]::Equals($expectedTimeSpan, $WaitTimeSpan) } -Times 1 -Scope 'Describe' + } + } - It 'Should call expected Mocks' { - Assert-VerifiableMocks - Assert-MockCalled ` - -CommandName Get-Service ` - -ParameterFilter { $Name -eq $script:testServiceName } ` - -Exactly 1 - } + Describe 'Service\Stop-ServiceWithTimeout' { + Mock -CommandName 'Stop-Service' -MockWith { } + Mock -CommandName 'Wait-ServiceStateWithTimeout' -MockWith { } + + $stopServiceWithTimeoutParameters = @{ + ServiceName = $script:testServiceName + TerminateTimeout = 500 + } + + $expectedTimeSpan = [TimeSpan]::FromMilliseconds($stopServiceWithTimeoutParameters.TerminateTimeout) + + It 'Should not throw' { + { Stop-ServiceWithTimeout @stopServiceWithTimeoutParameters } | Should Not Throw + } + + It 'Should stop service' { + Assert-MockCalled -CommandName 'Stop-Service' -ParameterFilter { $Name -eq $stopServiceWithTimeoutParameters.ServiceName } -Times 1 -Scope 'Describe' + } + + It 'Should wait for service to stop' { + Assert-MockCalled -CommandName 'Wait-ServiceStateWithTimeout' -ParameterFilter { $ServiceName -eq $stopServiceWithTimeoutParameters.ServiceName -and $State -eq [System.ServiceProcess.ServiceControllerStatus]::Stopped -and [TimeSpan]::Equals($expectedTimeSpan, $WaitTimeSpan) } -Times 1 -Scope 'Describe' } } }