diff --git a/.vscode/settings.json b/.vscode/settings.json index ea3c72550..6b806e216 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -67,7 +67,9 @@ "xSQLAOGroupEnsure", "Test-SPDSCObjectHasProperty", "DynamicAlloc", - "GetxPDTVariable" + "GetxPDTVariable", + "Dbcc", + "creplace" ], "cSpell.ignorePaths": [ ".git" diff --git a/CHANGELOG.md b/CHANGELOG.md index d19bec022..02f3e3f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 in desired state. - `ResourceBase` - class that can be inherited by class-based resource and provides functionality meant simplify the creating of class-based resource. + - `SqlResourceBase` - class that can be inherited by class-based resource and + provides default DSC properties and method for get a `[Server]`-object. - `ServerPermission` - complex type for the DSC resource SqlPermission. - The following private functions were added to the module (see comment-based help for more information): @@ -72,8 +74,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ConvertTo-SqlDscServerPermission` - `Get-SqlDscServerPermission` - `Set-SqlDscServerPermission` + - `Invoke-SqlDscQuery` + - `Get-SqlDscAudit` + - `New-SqlDscAudit` + - `Set-SqlDscAudit` + - `Remove-SqlDscAudit` + - `Enable-SqlDscAudit` + - `Disable-SqlDscAudit` - Support for debugging of integration tests in AppVeyor. - Only run for pull requests + - Add new resource SqlAudit. - CommonTestHelper - `Import-SqlModuleStub` - Added the optional parameter **PasThru** that, if used, will return the @@ -86,6 +96,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the PowerShell SqlServer stub module when a test has run. - SqlWindowsFirewall - Added integration tests for SqlWindowsFirewall ([issue #747](https://github.com/dsccommunity/SqlServerDsc/issues/747)). +- `Get-DscProperty` + - Added parameter `ExcludeName` to exclude property names from being returned. ### Changed @@ -296,8 +308,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed comment-based help and cleaned up comments. - Fix localized string that referenced 'user' instead of 'principal', and correct localized string ID for each string. + - Fix comment-based help. +- SqlPermission + - Fix comment-based help. - `Set-SqlDscDatabasePermission` - Minor code cleanup. +- `ConvertTo-Reason` + - Fix to handle `$null` values on Windows PowerShell. + - If the property name contain the word 'Path' the value will be parsed to + replace backslash or slashes at the end of the string, e.g. `'/mypath/'` + will become `'/mypath'`. +- `ResourceBase` + - Now handles `Ensure` correctly from derived `GetCurrentState()`. But + requires that the `GetCurrentState()` only return key property if object + is present, and does not return key property if object is absent. + Optionally the resource's derived `GetCurrentState()` can handle `Ensure` + itself. ## [15.2.0] - 2021-09-01 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d29d410c..fffaf128e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -329,11 +329,11 @@ if (-not $databaseExist) A terminating error is an error that the user are not able to ignore by passing a parameter to the command (like for non-terminating errors). -If a command shall throw an terminating error the statement `throw` shall -not be used, neither shall the command `Write-Error` be used with the parameter -`-ErrorAction `Stop``. Instead the method `$PSCmdlet.ThrowTerminatingError()` -shall be used to throw a terminating error. - +If a command shall throw an terminating error then the statement `throw` shall +not be used, neither shall the command `Write-Error` with the parameter +`-ErrorAction Stop`. Always use the method `$PSCmdlet.ThrowTerminatingError()` +to throw a terminating error. The exception is when a `[ValidateScript()]` +has to throw an error, then `throw` must be used. >**NOTE:** Below output assumes `$ErrorView` is set to `'NormalView'` in the >PowerShell session. diff --git a/appveyor.yml b/appveyor.yml index 307bb7a40..ecc4ad757 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,33 +23,27 @@ environment: #- TEST_CONFIGURATION: Integration_SQL2019 # DEBUG: See section on_finish last in this file on how to block build to keep RDP open. +# DEBUG: If running on own AppVeyor project, comment the line below that skips if it is not a pull request init: - ps: | # Only run for pull requests - if (-not $env:APPVEYOR_PULL_REQUEST_NUMBER) - { - return - } + if (-not $env:APPVEYOR_PULL_REQUEST_NUMBER) { Write-Host -ForegroundColor 'Yellow' -Object 'Not a pull request, skipping.'; return } iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) +# DEBUG: If running on own AppVeyor project, comment the line below that skips if it is not a pull request install: - ps: | # Only run for pull requests - if (-not $env:APPVEYOR_PULL_REQUEST_NUMBER) - { - return - } + if (-not $env:APPVEYOR_PULL_REQUEST_NUMBER) { Write-Host -ForegroundColor 'Yellow' -Object 'Not a pull request, skipping.'; return } winrm quickconfig -quiet +# DEBUG: If running on own AppVeyor project, comment the line below that skips if it is not a pull request build_script: - pwsh: | # Only run for pull requests - if (-not $env:APPVEYOR_PULL_REQUEST_NUMBER) - { - return - } + if (-not $env:APPVEYOR_PULL_REQUEST_NUMBER) { Write-Host -ForegroundColor 'Yellow' -Object 'Not a pull request, skipping.'; return } # Build the module ./build.ps1 -ResolveDependency -tasks build @@ -57,13 +51,11 @@ build_script: # DEBUG: Comment and un-comment integration tests as needed for the purpose of debugging. # Note that some integration tests depend on each other to work. See the README for more # information: https://github.com/dsccommunity/SqlServerDsc/blob/main/tests/Integration/README.md +# DEBUG: If running on own AppVeyor project, comment the line below that skips if it is not a pull request test_script: - ps: | # Only run for pull requests - if (-not $env:APPVEYOR_PULL_REQUEST_NUMBER) - { - return - } + if (-not $env:APPVEYOR_PULL_REQUEST_NUMBER) { Write-Host -ForegroundColor 'Yellow' -Object 'Not a pull request, skipping.'; return } ./build.ps1 -Tasks test -CodeCoverageThreshold 0 -PesterTag $env:TEST_CONFIGURATION -PesterPath @( ### Run the integration tests in a specific group order. @@ -86,6 +78,7 @@ test_script: 'tests/Integration/DSC_SqlRS.Integration.Tests.ps1' #'tests/Integration/DSC_SqlDatabaseUser.Integration.Tests.ps1' #'tests/Integration/DSC_SqlReplication.Integration.Tests.ps1' + #'tests/Integration/DSC_SqlAudit.Integration.Tests.ps1' ## Group 4 #'tests/Integration/DSC_SqlScript.Integration.Tests.ps1' #'tests/Integration/DSC_SqlDatabasePermission.Integration.Tests.ps1' @@ -102,8 +95,11 @@ test_script: deploy: off -# DEBUG: Un-comment the following line so that build worker is kept up all of the 60 minutes. +# DEBUG: Un-comment the line "$blockRdp = $true" so that build worker is kept up all of the 60 minutes. +# DEBUG: If running on own AppVeyor project, comment the line below that skips if it is not a pull request on_finish: - ps: | + if (-not $env:APPVEYOR_PULL_REQUEST_NUMBER) { Write-Host -ForegroundColor 'Yellow' -Object 'Not a pull request, skipping.'; return } + #$blockRdp = $true - iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + iex ((New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 647d85ae9..844d7d014 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -87,6 +87,7 @@ stages: - task: PowerShell@2 name: test displayName: 'Run HQRM Test' + condition: succeededOrFailed() inputs: filePath: './build.ps1' arguments: '-Tasks hqrmtest' @@ -197,6 +198,7 @@ stages: 'tests/Integration/DSC_SqlRS.Integration.Tests.ps1' 'tests/Integration/DSC_SqlDatabaseUser.Integration.Tests.ps1' 'tests/Integration/DSC_SqlReplication.Integration.Tests.ps1' + 'tests/Integration/DSC_SqlAudit.Integration.Tests.ps1' # Group 4 'tests/Integration/DSC_SqlScript.Integration.Tests.ps1' 'tests/Integration/DSC_SqlDatabasePermission.Integration.Tests.ps1' diff --git a/build.yaml b/build.yaml index 40e6d57a3..23bd2c55c 100644 --- a/build.yaml +++ b/build.yaml @@ -24,6 +24,7 @@ BuildWorkflow: test: - Pester_Tests_Stop_On_Fail + - Convert_Pester_Coverage - Pester_If_Code_Coverage_Under_Threshold publish: diff --git a/source/Classes/010.ResourceBase.ps1 b/source/Classes/010.ResourceBase.ps1 index b0a730ccf..642bd7b56 100644 --- a/source/Classes/010.ResourceBase.ps1 +++ b/source/Classes/010.ResourceBase.ps1 @@ -52,6 +52,8 @@ class ResourceBase } } + $keyPropertyAddedToCurrentState = $false + # Set key property values unless it was returned from the derived class' GetCurrentState(). foreach ($propertyName in $keyProperty.Keys) { @@ -59,79 +61,36 @@ class ResourceBase { # Add the key value to the instance to be returned. $dscResourceObject.$propertyName = $this.$propertyName - } - } - - $ignoreProperty = @() - <# - TODO: This need to be re-evaluated for a resource that is using Ensure - property. How Ensure is handled might need to be refactored, or - removed altogether from this base class. - - If the derived DSC resource has a Ensure property and it was not returned - by GetCurrentState(), then the property Ensure is removed from the - comparison (when calling Compare()). The property Ensure is ignored - since the method GetCurrentState() did not return it, and the current - state for property Ensure cannot be determined until the method Compare() - has run to determined if other properties are not in desired state. - #> - if (($this | Test-ResourceHasDscProperty -Name 'Ensure') -and -not $getCurrentStateResult.ContainsKey('Ensure')) - { - $ignoreProperty += 'Ensure' + $keyPropertyAddedToCurrentState = $true + } } - <# - Returns all enforced properties not in desires state, or $null if - all enforced properties are in desired state. - #> - $propertiesNotInDesiredState = $this.Compare($getCurrentStateResult, $ignoreProperty) - - <# - Return the correct values for Ensure property if the derived DSC resource - has such property and it hasn't been already set by GetCurrentState(). - #> if (($this | Test-ResourceHasDscProperty -Name 'Ensure') -and -not $getCurrentStateResult.ContainsKey('Ensure')) { - if ($propertiesNotInDesiredState) + # Evaluate if we should set Ensure property. + if ($keyPropertyAddedToCurrentState) { <# - Get all the key properties that might not be in desired state. - This will return $null if all key properties are in desired state. + A key property was added to the current state, assume its because + the object did not exist in the current state. Set Ensure to Absent. #> - $keyPropertiesNotInDesiredState = $this | Get-DscProperty -Name $propertiesNotInDesiredState.Property -Type 'Key' - - if ($keyPropertiesNotInDesiredState) - { - <# - The compare come back with at least one key property that was - not in desired state. That only happens if the object does not - exist on the node, so the Ensure value is set to Absent since - the object does not exist. - #> - $dscResourceObject.Ensure = [Ensure]::Absent - } - else - { - <# - The compare come back with all key properties in desired state. - That only happens if the object exist on the node, so the Ensure - value is set to Present since the object exist. - #> - $dscResourceObject.Ensure = [Ensure]::Present - } + $dscResourceObject.Ensure = [Ensure]::Absent + $getCurrentStateResult.Ensure = [Ensure]::Absent } else { - <# - The compare come back with $null, meaning that all key properties - match. That only happens if the object exist on the node, so the - Ensure value is set to Present since the object exist. - #> $dscResourceObject.Ensure = [Ensure]::Present + $getCurrentStateResult.Ensure = [Ensure]::Present } } + <# + Returns all enforced properties not in desires state, or $null if + all enforced properties are in desired state. + #> + $propertiesNotInDesiredState = $this.Compare($getCurrentStateResult, @()) + <# Return the correct values for Reasons property if the derived DSC resource has such property and it hasn't been already set by GetCurrentState(). @@ -140,7 +99,7 @@ class ResourceBase { # Always return an empty array if all properties are in desired state. $dscResourceObject.Reasons = $propertiesNotInDesiredState | - ConvertTo-Reason -ResourceName $this.GetType() + ConvertTo-Reason -ResourceName $this.GetType().Name } # Return properties. diff --git a/source/Classes/011.SqlResourceBase.ps1 b/source/Classes/011.SqlResourceBase.ps1 new file mode 100644 index 000000000..dde29020c --- /dev/null +++ b/source/Classes/011.SqlResourceBase.ps1 @@ -0,0 +1,81 @@ +<# + .SYNOPSIS + The SqlResource base have generic properties and methods for the class-based + resources. + + .PARAMETER InstanceName + The name of the _SQL Server_ instance to be configured. Default value is + `'MSSQLSERVER'`. + + .PARAMETER ServerName + The host name of the _SQL Server_ to be configured. Default value is the + current computer name. + + .PARAMETER Credential + Specifies the credential to use to connect to the _SQL Server_ instance. + + If parameter **Credential'* is not provided then the resource instance is + run using the credential that runs the configuration. + + .PARAMETER Reasons + Returns the reason a property is not in desired state. +#> +class SqlResourceBase : ResourceBase +{ + <# + Property for holding the server connection object. + This should be an object of type [Microsoft.SqlServer.Management.Smo.Server] + but using that type fails the build process currently. + See issue https://github.com/dsccommunity/DscResource.DocGenerator/issues/121. + #> + hidden [System.Object] $SqlServerObject + + [DscProperty(Key)] + [System.String] + $InstanceName + + [DscProperty()] + [System.String] + $ServerName = (Get-ComputerName) + + [DscProperty()] + [PSCredential] + $Credential + + [DscProperty(NotConfigurable)] + [Reason[]] + $Reasons + + SqlResourceBase () : base () + { + $this.SqlServerObject = $null + } + + <# + Returns and reuses the server connection object. If the server connection + object does not exist a connection to the SQL Server instance will occur. + + This should return an object of type [Microsoft.SqlServer.Management.Smo.Server] + but using that type fails the build process currently. + See issue https://github.com/dsccommunity/DscResource.DocGenerator/issues/121. + #> + hidden [System.Object] GetServerObject() + { + if (-not $this.SqlServerObject) + { + $connectSqlDscDatabaseEngineParameters = @{ + ServerName = $this.ServerName + InstanceName = $this.InstanceName + } + + if ($this.Credential) + { + $connectSqlDscDatabaseEngineParameters.Credential = $this.Credential + } + + $this.SqlServerObject = Connect-SqlDscDatabaseEngine @connectSqlDscDatabaseEngineParameters + } + + return $this.SqlServerObject + } +} diff --git a/source/Classes/020.SqlAudit.ps1 b/source/Classes/020.SqlAudit.ps1 new file mode 100644 index 000000000..a0ac2bc44 --- /dev/null +++ b/source/Classes/020.SqlAudit.ps1 @@ -0,0 +1,594 @@ +<# + .SYNOPSIS + The `SqlAudit` DSC resource is used to create, modify, or remove + server audits. + + .DESCRIPTION + The `SqlAudit` DSC resource is used to create, modify, or remove + server audits. + + The built-in parameter **PSDscRunAsCredential** can be used to run the resource + as another user. The resource will then authenticate to the SQL Server + instance as that user. It also possible to instead use impersonation by the + parameter **Credential**. + + ## Requirements + + * Target machine must be running Windows Server 2012 or later. + * Target machine must be running SQL Server Database Engine 2012 or later. + * Target machine must have access to the SQLPS PowerShell module or the SqlServer + PowerShell module. + + ## Known issues + + All issues are not listed here, see [here for all open issues](https://github.com/dsccommunity/SqlServerDsc/issues?q=is%3Aissue+is%3Aopen+in%3Atitle+SqlAudit). + + ### Property **Reasons** does not work with **PSDscRunAsCredential** + + When using the built-in parameter `**PSDscRunAsCredential** the read-only + property `Reasons` will return empty values for Code and Phrase. The + built-in property **PSDscRunAsCredential** does not work with class-based + resources that using advanced type like the parameter `Reasons` have. + + ### Using **Credential** property. + + SQL Authentication and Group Managed Service Accounts is not supported as + impersonation credentials. Currently only Windows Integrated Security is + supported to use as credentials. + + For Windows Authentication the username must either be provided with the User + Principal Name (UPN), e.g. 'username@domain.local' or if using non-domain + (for example a local Windows Server account) account the username must be + provided without the NetBIOS name, e.g. 'username'. The format 'DOMAIN\username' + will not work. + + See more information in [Credential Overview](https://github.com/dsccommunity/SqlServerDsc/wiki/CredentialOverview). + + .PARAMETER Name + The name of the audit. + + .PARAMETER LogType + Specifies the to which log an audit logs to. Mutually exclusive to parameter + **Path**. + + .PARAMETER Path + Specifies the destination path for a file audit. Mutually exclusive to parameter + **LogType**. + + .PARAMETER Filter + Specifies the filter that should be used on the audit. + + .PARAMETER MaximumFiles + Specifies the number of files on disk. Mutually exclusive to parameter + **MaximumRolloverFiles**. Mutually exclusive to parameter **LogType**. + + .PARAMETER MaximumFileSize + Specifies the maximum file size in units by parameter **MaximumFileSizeUnit**. + If this is specified the parameter **MaximumFileSizeUnit** must also be + specified. Mutually exclusive to parameter **LogType**. Minimum allowed value + is 2 (MB). It also allowed to set the value to 0 which mean unlimited file + size. + + .PARAMETER MaximumFileSizeUnit + Specifies the unit that is used for the file size. + If this is specified the parameter **MaximumFileSize** must also be + specified. Mutually exclusive to parameter **LogType**. + + .PARAMETER MaximumRolloverFiles + Specifies the amount of files on disk before SQL Server starts reusing + the files. Mutually exclusive to parameter **MaximumFiles** and **LogType**. + + .PARAMETER OnFailure + Specifies what should happen when writing events to the store fails. + This can be `Continue`, `FailOperation`, or `Shutdown`. + + .PARAMETER QueueDelay + Specifies the maximum delay before a event is written to the store. + When set to low this could impact server performance. + When set to high events could be missing when a server crashes. + + .PARAMETER ReserveDiskSpace + Specifies if the needed file space should be reserved. only needed + when writing to a file log. Mutually exclusive to parameter **LogType**. + + .PARAMETER Enabled + Specifies if the audit should be enabled. Defaults to `$false`. + + .PARAMETER Ensure + Specifies if the server audit should be present or absent. If set to `Present` + the audit will be added if it does not exist, or updated if the audit exist. + If `Absent` then the audit will be removed from the server. Defaults to + `Present`. + + .PARAMETER Force + Specifies if it is allowed to re-create the server audit if a current audit + exist with the same name but of a different audit type. Defaults to `$false` + not allowing server audits to be re-created. + + .EXAMPLE + Invoke-DscResource -ModuleName SqlServerDsc -Name SqlAudit -Method Get -Property @{ + ServerName = 'localhost' + InstanceName = 'SQL2017' + Credential = (Get-Credential -UserName 'myuser@company.local' -Message 'Password:') + Name = 'Log1' + } + + This example shows how to call the resource using Invoke-DscResource. +#> +[DscResource(RunAsCredential = 'Optional')] +class SqlAudit : SqlResourceBase +{ + [DscProperty(Key)] + [System.String] + $Name + + [DscProperty()] + [ValidateSet('SecurityLog', 'ApplicationLog')] + [System.String] + $LogType + + # The Path is evaluated if exist in AssertProperties(). + [DscProperty()] + [System.String] + $Path + + [DscProperty()] + [System.String] + $AuditFilter + + [DscProperty()] + [Nullable[System.UInt32]] + $MaximumFiles + + [DscProperty()] + # There is an extra evaluation in AssertProperties() for this range. + [ValidateRange(0, 2147483647)] + [Nullable[System.UInt32]] + $MaximumFileSize + + [DscProperty()] + [ValidateSet('Megabyte', 'Gigabyte', 'Terabyte')] + [System.String] + $MaximumFileSizeUnit + + [DscProperty()] + [ValidateRange(0, 2147483647)] + [Nullable[System.UInt32]] + $MaximumRolloverFiles + + [DscProperty()] + [ValidateSet('Continue', 'FailOperation', 'Shutdown')] + [System.String] + $OnFailure + + [DscProperty()] + # There is an extra evaluation in AssertProperties() for this range. + [ValidateRange(0, 2147483647)] + [Nullable[System.UInt32]] + $QueueDelay + + [DscProperty()] + [ValidatePattern('^[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')] + [System.String] + $AuditGuid + + [DscProperty()] + [Nullable[System.Boolean]] + $ReserveDiskSpace + + [DscProperty()] + [Nullable[System.Boolean]] + $Enabled + + [DscProperty()] + [Ensure] + $Ensure = [Ensure]::Present + + [DscProperty()] + [Nullable[System.Boolean]] + $Force + + SqlAudit () : base () + { + # TODO:_Rename this to ExcludeDscProperties or ExcludeProperties + # These properties will not be enforced. + $this.notEnforcedProperties = @( + 'ServerName' + 'InstanceName' + 'Name' + 'Credential' + 'Force' + ) + } + + [SqlAudit] Get() + { + # Call the base method to return the properties. + return ([ResourceBase] $this).Get() + } + + [System.Boolean] Test() + { + # Call the base method to test all of the properties that should be enforced. + return ([ResourceBase] $this).Test() + } + + [void] Set() + { + # Call the base method to enforce the properties. + ([ResourceBase] $this).Set() + } + + <# + Base method Get() call this method to get the current state as a hashtable. + The parameter properties will contain the key properties. + #> + hidden [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) + { + Write-Verbose -Message ( + $this.localizedData.EvaluateServerAudit -f @( + $properties.Name, + $properties.InstanceName + ) + ) + + $currentStateCredential = $null + + if ($this.Credential) + { + <# + This does not work, even if username is set, the method Get() will + return an empty PSCredential-object. Kept it here so it at least + return a Credential object. + #> + $currentStateCredential = [PSCredential]::new( + $this.Credential.UserName, + [SecureString]::new() + ) + } + + <# + Only set key property Name if the audit exist. Base class will set it + and handle Ensure. + #> + $currentState = @{ + Credential = $currentStateCredential + InstanceName = $properties.InstanceName + ServerName = $this.ServerName + Force = $this.Force + } + + $serverObject = $this.GetServerObject() + + $auditObject = $serverObject | + Get-SqlDscAudit -Name $properties.Name -ErrorAction 'SilentlyContinue' + + if ($auditObject) + { + $currentState.Name = $properties.Name + + if ($auditObject.DestinationType -in @('ApplicationLog', 'SecurityLog')) + { + $currentState.LogType = $auditObject.DestinationType + } + + if ($auditObject.FilePath) + { + # Remove trailing slash or backslash. + $currentState.Path = $auditObject.FilePath -replace '[\\|/]*$' + } + + $currentState.AuditFilter = $auditObject.Filter + $currentState.MaximumFiles = [System.UInt32] $auditObject.MaximumFiles + $currentState.MaximumFileSize = [System.UInt32] $auditObject.MaximumFileSize + + # The value of MaximumFileSizeUnit can be zero, so have to check against $null + if ($null -ne $auditObject.MaximumFileSizeUnit) + { + $convertedMaximumFileSizeUnit = ( + @{ + 0 = 'Megabyte' + 1 = 'Gigabyte' + 2 = 'Terabyte' + } + ).($auditObject.MaximumFileSizeUnit.value__) + + $currentState.MaximumFileSizeUnit = $convertedMaximumFileSizeUnit + } + + $currentState.MaximumRolloverFiles = [System.UInt32] $auditObject.MaximumRolloverFiles + $currentState.OnFailure = $auditObject.OnFailure + $currentState.QueueDelay = [System.UInt32] $auditObject.QueueDelay + $currentState.AuditGuid = $auditObject.Guid + $currentState.ReserveDiskSpace = $auditObject.ReserveDiskSpace + $currentState.Enabled = $auditObject.Enabled + } + + return $currentState + } + + <# + Base method Set() call this method with the properties that should be + enforced are not in desired state. It is not called if all properties + are in desired state. The variable $properties contain the properties + that are not in desired state. + #> + hidden [void] Modify([System.Collections.Hashtable] $properties) + { + $serverObject = $this.GetServerObject() + $auditObject = $null + + if ($properties.Keys -contains 'Ensure') + { + # Evaluate the desired state for property Ensure. + switch ($properties.Ensure) + { + 'Present' + { + # Create the audit since it was missing. Always created disabled. + $auditObject = $this.CreateAudit() + } + + 'Absent' + { + # Remove the audit since it was present + $serverObject | Remove-SqlDscAudit -Name $this.Name -Force + } + } + } + else + { + <# + Update any properties not in desired state if the audit should be present. + At this point it is assumed the audit exist since Ensure property was + in desired state. + + If the desired state happens to be Absent then ignore any properties not + in desired state (user have in that case wrongly added properties to an + "absent configuration"). + #> + if ($this.Ensure -eq [Ensure]::Present) + { + $auditObject = $serverObject | + Get-SqlDscAudit -Name $this.Name -ErrorAction 'Stop' + + if ($auditObject) + { + $auditIsWrongType = ( + # If $auditObject.DestinationType is not null. + $null -ne $auditObject.DestinationType -and ( + # Path is not in desired state but the audit is not of type File. + $properties.ContainsKey('Path') -and $auditObject.DestinationType -ne 'File' + ) -or ( + # LogType is not in desired state but the audit is of type File. + $properties.ContainsKey('LogType') -and $auditObject.DestinationType -eq 'File' + ) + ) + + # Does the audit need to be re-created? + if ($auditIsWrongType) + { + if ($this.Force -eq $true) + { + $auditObject | Remove-SqlDscAudit -Force + + $auditObject = $this.CreateAudit() + } + else + { + New-InvalidOperationException -Message $this.localizedData.AuditIsWrongType + } + } + else + { + <# + Should evaluate DestinationType so that is does not try to set a + File audit property when audit type is of a Log-type. + #> + if ($null -ne $auditObject.DestinationType -and $auditObject.DestinationType -ne 'File') + { + # Look for file audit properties not in desired state + $fileAuditProperty = $properties.Keys.Where({ + $_ -in @( + 'Path' + 'MaximumFiles' + 'MaximumFileSize' + 'MaximumFileSizeUnit' + 'MaximumRolloverFiles' + 'ReserveDiskSpace' + ) + }) + + # If a property was found, throw an exception. + if ($fileAuditProperty.Count -gt 0) + { + New-InvalidOperationException -Message ($this.localizedData.AuditOfWrongTypeForUseWithProperty -f $auditObject.DestinationType) + } + } + + # Get all optional properties that has an assigned value. + $assignedOptionalDscProperties = $this | Get-DscProperty -HasValue -Type 'Optional' -ExcludeName @( + # Remove optional properties that is not an audit property. + 'ServerName' + 'Ensure' + 'Force' + 'Credential' + + # Remove this audit property since it must be handled later. + 'Enabled' + ) + + <# + Only call Set when there is a property to Set. The property + Enabled was ignored, so it could be the only one that was + not in desired state (Enabled is handled later). + #> + if ($assignedOptionalDscProperties.Count -gt 0) + { + <# + This calls Set-SqlDscAudit to set all the desired value + even if they were in desired state. Then the no logic is + needed to make sure we call using the correct parameter + set that Set-SqlDscAudit requires. + #> + $auditObject | Set-SqlDscAudit @assignedOptionalDscProperties -Force + } + } + } + } + } + + <# + If there is an audit object either from a newly created or fetched from + current state, and if the desired state is Present, evaluate if the + audit should be enable or disable. + #> + if ($auditObject -and $this.Ensure -eq [Ensure]::Present -and $properties.Keys -contains 'Enabled') + { + switch ($properties.Enabled) + { + $true + { + $serverObject | Enable-SqlDscAudit -Name $this.Name -Force + } + + $false + { + $serverObject | Disable-SqlDscAudit -Name $this.Name -Force + } + } + } + } + + <# + Base method Assert() call this method with the properties that was assigned + a value. + #> + hidden [void] AssertProperties([System.Collections.Hashtable] $properties) + { + # The properties MaximumFiles and MaximumRolloverFiles are mutually exclusive. + $assertBoundParameterParameters = @{ + BoundParameterList = $properties + MutuallyExclusiveList1 = @( + 'MaximumFiles' + ) + MutuallyExclusiveList2 = @( + 'MaximumRolloverFiles' + ) + } + + Assert-BoundParameter @assertBoundParameterParameters + + # LogType is mutually exclusive from any of the File audit properties. + $assertBoundParameterParameters = @{ + BoundParameterList = $properties + MutuallyExclusiveList1 = @( + 'LogType' + ) + MutuallyExclusiveList2 = @( + 'Path' + 'MaximumFiles' + 'MaximumFileSize' + 'MaximumFileSizeUnit' + 'MaximumRolloverFiles' + 'ReserveDiskSpace' + ) + } + + Assert-BoundParameter @assertBoundParameterParameters + + # Get all assigned *FileSize properties. + $assignedSizeProperty = $properties.Keys.Where({ + $_ -in @( + 'MaximumFileSize', + 'MaximumFileSizeUnit' + ) + }) + + <# + Neither or both of the properties MaximumFileSize and MaximumFileSizeUnit + must be assigned. + #> + if ($assignedSizeProperty.Count -eq 1) + { + $errorMessage = $this.localizedData.BothFileSizePropertiesMustBeSet + + New-InvalidArgumentException -ArgumentName 'MaximumFileSize, MaximumFileSizeUnit' -Message $errorMessage + } + + <# + Since we cannot use [ValidateScript()], and it is no possible to exclude + a value in the [ValidateRange()], evaluate so 1 is not assigned. + #> + if ($properties.Keys -contains 'MaximumFileSize' -and $properties.MaximumFileSize -eq 1) + { + $errorMessage = $this.localizedData.MaximumFileSizeValueInvalid + + New-InvalidArgumentException -ArgumentName 'MaximumFileSize' -Message $errorMessage + } + + <# + Since we cannot use [ValidateScript()], and it is no possible to exclude + a value in the [ValidateRange()], evaluate so 1-999 is not assigned. + #> + if ($properties.Keys -contains 'QueueDelay' -and $properties.QueueDelay -in 1..999) + { + $errorMessage = $this.localizedData.QueueDelayValueInvalid + + New-InvalidArgumentException -ArgumentName 'QueueDelay' -Message $errorMessage + } + + # ReserveDiskSpace can only be used with MaximumFiles. + if ($properties.Keys -contains 'ReserveDiskSpace' -and $properties.Keys -notcontains 'MaximumFiles') + { + $errorMessage = $this.localizedData.BothFileSizePropertiesMustBeSet + + New-InvalidArgumentException -ArgumentName 'ReserveDiskSpace' -Message $errorMessage + } + + # Test so that the path exist. + if ($properties.Keys -contains 'Path' -and -not (Test-Path -Path $properties.Path)) + { + $errorMessage = $this.localizedData.PathInvalid -f $properties.Path + + New-InvalidArgumentException -ArgumentName 'Path' -Message $errorMessage + } + } + + <# + Create and returns the desired audit object. Always created disabled. + + This should return an object of type [Microsoft.SqlServer.Management.Smo.Audit] + but using that type fails the build process currently. + See issue https://github.com/dsccommunity/DscResource.DocGenerator/issues/121. + #> + hidden [System.Object] CreateAudit() + { + # Get all properties that has an assigned value. + $assignedDscProperties = $this | Get-DscProperty -HasValue -Type @( + 'Key' + 'Optional' + ) -ExcludeName @( + # Remove properties that is not an audit property. + 'InstanceName' + 'ServerName' + 'Ensure' + 'Force' + 'Credential' + + # Remove this audit property since it must be handled later. + 'Enabled' + ) + + if ($assignedDscProperties.Keys -notcontains 'LogType' -and $assignedDscProperties.Keys -notcontains 'Path') + { + New-InvalidOperationException -Message $this.localizedData.CannotCreateNewAudit + } + + $serverObject = $this.GetServerObject() + + $auditObject = $serverObject | New-SqlDscAudit @assignedDscProperties -Force -PassThru + + return $auditObject + } +} diff --git a/source/Classes/020.SqlDatabasePermission.ps1 b/source/Classes/020.SqlDatabasePermission.ps1 index e89f97705..fa94dfbfa 100644 --- a/source/Classes/020.SqlDatabasePermission.ps1 +++ b/source/Classes/020.SqlDatabasePermission.ps1 @@ -24,8 +24,8 @@ ### `PSDscRunAsCredential` not supported The built-in property `PSDscRunAsCredential` does not work with class-based - resources that using advanced type like the parameter `Permission` does. - Use the parameter `Credential` instead of `PSDscRunAsCredential`. + resources that using advanced type like the parameters `Permission` and + `Reasons` has. Use the parameter `Credential` instead of `PSDscRunAsCredential`. ### Using `Credential` property. diff --git a/source/Classes/020.SqlPermission.ps1 b/source/Classes/020.SqlPermission.ps1 index cb69a7626..696b7fcbe 100644 --- a/source/Classes/020.SqlPermission.ps1 +++ b/source/Classes/020.SqlPermission.ps1 @@ -26,8 +26,8 @@ ### `PSDscRunAsCredential` not supported The built-in property `PSDscRunAsCredential` does not work with class-based - resources that using advanced type like the parameter `Permission` does. - Use the parameter `Credential` instead of `PSDscRunAsCredential`. + resources that using advanced type like the parameters `Permission` and + `Reasons` has. Use the parameter `Credential` instead of `PSDscRunAsCredential`. ### Using `Credential` property. diff --git a/source/Examples/README.md b/source/Examples/README.md index e209a042f..60629599c 100644 --- a/source/Examples/README.md +++ b/source/Examples/README.md @@ -15,6 +15,7 @@ These are the links to the examples for each individual resource. - [SqlAGReplica](Resources/SqlAGReplica) - [SqlAlias](Resources/SqlAlias) - [SqlAlwaysOnService](Resources/SqlAlwaysOnService) +- [SqlAudit](Resources/SqlAudit) - [SqlDatabase](Resources/SqlDatabase) - [SqlDatabaseDefaultLocation](Resources/SqlDatabaseDefaultLocation) - [SqlDatabasePermission](Resources/SqlDatabasePermission) diff --git a/source/Examples/Resources/SqlAudit/1-AddFileAudit.ps1 b/source/Examples/Resources/SqlAudit/1-AddFileAudit.ps1 new file mode 100644 index 000000000..f0d3438e6 --- /dev/null +++ b/source/Examples/Resources/SqlAudit/1-AddFileAudit.ps1 @@ -0,0 +1,33 @@ +<# + .EXAMPLE + This example shows how to ensure that an audit destination + is absent on the instance sqltest.company.local\DSC. +#> +Configuration Example +{ + param + ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $SqlAdministratorCredential + ) + + Import-DscResource -ModuleName SqlServerDsc + + node localhost + { + SqlAudit FileAudit_Server + { + Ensure = 'Present' + ServerName = 'SQL2019-01' + InstanceName = 'INST01' + Name = 'FileAudit' + Path = 'C:\Temp\audit' + MaximumFileSize = 10 + MaximumFileSizeUnit = 'Megabyte' + MaximumRolloverFiles = 11 + Enabled = $true + Credential = $SqlAdministratorCredential + } + } +} diff --git a/source/Examples/Resources/SqlAudit/2-AddSecuritylogAudit.ps1 b/source/Examples/Resources/SqlAudit/2-AddSecuritylogAudit.ps1 new file mode 100644 index 000000000..178da00eb --- /dev/null +++ b/source/Examples/Resources/SqlAudit/2-AddSecuritylogAudit.ps1 @@ -0,0 +1,30 @@ +<# + .EXAMPLE + This example shows how to ensure that the windows security event log + audit destination is present on the instance sqltest.company.local\DSC. +#> +Configuration Example +{ + param + ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $SqlAdministratorCredential + ) + + Import-DscResource -ModuleName SqlServerDsc + + node localhost + { + SqlAudit SecurityLogAudit_Server + { + Ensure = 'Present' + ServerName = 'sqltest.company.local' + InstanceName = 'DSC' + Name = 'SecLogAudit' + LogType = 'SecurityLog' + Enabled = $true + Credential = $SqlAdministratorCredential + } + } +} diff --git a/source/Examples/Resources/SqlAudit/3-AddSecuritylogAuditOnFailureShutdown.ps1 b/source/Examples/Resources/SqlAudit/3-AddSecuritylogAuditOnFailureShutdown.ps1 new file mode 100644 index 000000000..84eeac510 --- /dev/null +++ b/source/Examples/Resources/SqlAudit/3-AddSecuritylogAuditOnFailureShutdown.ps1 @@ -0,0 +1,31 @@ +<# + .EXAMPLE + This example shows how to ensure that the windows security event log + audit destination is present on the instance sqltest.company.local\DSC. + The server should shutdown when logging is not possible. +#> +Configuration Example +{ + param + ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $SqlAdministratorCredential + ) + + Import-DscResource -ModuleName SqlServerDsc + + node localhost + { + SqlAudit SecurityLogAudit_Server + { + Ensure = 'Present' + ServerName = 'sqltest.company.local' + InstanceName = 'DSC' + Name = 'SecLogAudit' + OnFailure = 'Shutdown' + Enabled = $true + Credential = $SqlAdministratorCredential + } + } +} diff --git a/source/Examples/Resources/SqlAudit/4-AddSecuritylogAuditWithFilter.ps1 b/source/Examples/Resources/SqlAudit/4-AddSecuritylogAuditWithFilter.ps1 new file mode 100644 index 000000000..1847c2b0e --- /dev/null +++ b/source/Examples/Resources/SqlAudit/4-AddSecuritylogAuditWithFilter.ps1 @@ -0,0 +1,32 @@ +<# + .EXAMPLE + This example shows how to ensure that the windows security event log + audit destination is present on the instance sqltest.company.local\DSC. + and adds a filter so only users with a name lie administrator are audited +#> +Configuration Example +{ + param + ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $SqlAdministratorCredential + ) + + Import-DscResource -ModuleName SqlServerDsc + + node localhost + { + SqlAudit SecurityLogAudit_Server + { + Ensure = 'Present' + ServerName = 'sqltest.company.local' + InstanceName = 'DSC' + Name = 'SecLogAudit' + LogType = 'SecurityLog' + Enabled = $true + AuditFilter = '([server_principal_name] like ''%ADMINISTRATOR'')' + Credential = $SqlAdministratorCredential + } + } +} diff --git a/source/Examples/Resources/SqlAudit/5-RemoveAudit.ps1 b/source/Examples/Resources/SqlAudit/5-RemoveAudit.ps1 new file mode 100644 index 000000000..c963b451f --- /dev/null +++ b/source/Examples/Resources/SqlAudit/5-RemoveAudit.ps1 @@ -0,0 +1,28 @@ +<# + .EXAMPLE + This example shows how to ensure that an audit destination + is absent on the instance sqltest.company.local\DSC. +#> +Configuration Example +{ + param + ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $SqlAdministratorCredential + ) + + Import-DscResource -ModuleName SqlServerDsc + + node localhost + { + SqlAudit FileAudit_Server + { + Ensure = 'Absent' + ServerName = 'sqltest.company.local' + InstanceName = 'DSC' + Name = 'FileAudit' + Credential = $SqlAdministratorCredential + } + } +} diff --git a/source/Private/ConvertTo-Reason.ps1 b/source/Private/ConvertTo-Reason.ps1 index fe5c2e0cb..658627360 100644 --- a/source/Private/ConvertTo-Reason.ps1 +++ b/source/Private/ConvertTo-Reason.ps1 @@ -71,10 +71,43 @@ function ConvertTo-Reason $propertyActualValue = $currentProperty.ActualValue } + <# + In PowerShell 7 the command ConvertTo-Json returns 'null' on null + value, but not in Windows PowerShell. Switch to output empty string + if value is null. + #> + if ($PSVersionTable.PSEdition -eq 'Desktop') + { + if ($null -eq $propertyExpectedValue) + { + $propertyExpectedValue = '' + } + + if ($null -eq $propertyActualValue) + { + $propertyActualValue = '' + } + } + + # Convert the value to Json to be able to easily visualize complex types + $propertyActualValueJson = $propertyActualValue | ConvertTo-Json -Compress + $propertyExpectedValueJson = $propertyExpectedValue | ConvertTo-Json -Compress + + # If the property name contain the word Path, remove '\\' from path. + if ($currentProperty.Property -match 'Path') + { + $propertyActualValueJson = $propertyActualValueJson -replace '\\\\', '\' + $propertyExpectedValueJson = $propertyExpectedValueJson -replace '\\\\', '\' + } + $reasons += [Reason] @{ Code = '{0}:{0}:{1}' -f $ResourceName, $currentProperty.Property # Convert the object to JSON to handle complex types. - Phrase = 'The property {0} should be {1}, but was {2}' -f $currentProperty.Property, ($propertyExpectedValue | ConvertTo-Json -Compress), ($propertyActualValue | ConvertTo-Json -Compress) + Phrase = 'The property {0} should be {1}, but was {2}' -f @( + $currentProperty.Property, + $propertyExpectedValueJson, + $propertyActualValueJson + ) } } } diff --git a/source/Private/Get-DscProperty.ps1 b/source/Private/Get-DscProperty.ps1 index b77ffa9dd..481842c84 100644 --- a/source/Private/Get-DscProperty.ps1 +++ b/source/Private/Get-DscProperty.ps1 @@ -61,6 +61,10 @@ function Get-DscProperty [System.String[]] $Name, + [Parameter()] + [System.String[]] + $ExcludeName, + [Parameter()] [ValidateSet('Key', 'Mandatory', 'NotConfigurable', 'Optional')] [System.String[]] @@ -79,6 +83,12 @@ function Get-DscProperty #> (-not $Name -or $_ -in $Name) -and + <# + Return all properties if $ExcludeName is not assigned. Skip + property if it is included in $ExcludeName. + #> + (-not $ExcludeName -or ($_ -notin $ExcludeName)) -and + # Only return the property if it is a DSC property. $InputObject.GetType().GetMember($_).CustomAttributes.Where( { diff --git a/source/Public/Disable-SqlDscAudit.ps1 b/source/Public/Disable-SqlDscAudit.ps1 new file mode 100644 index 000000000..c77eef43d --- /dev/null +++ b/source/Public/Disable-SqlDscAudit.ps1 @@ -0,0 +1,94 @@ +<# + .SYNOPSIS + Disables a server audit. + + .PARAMETER ServerObject + Specifies current server connection object. + + .PARAMETER AuditObject + Specifies an audit object to disable. + + .PARAMETER Name + Specifies the name of the server audit to be disabled. + + .PARAMETER Force + Specifies that the audit should be disabled with out any confirmation. + + .PARAMETER Refresh + Specifies that the **ServerObject**'s audits should be refreshed before + trying to disable the audit object. This is helpful when audits could have + been modified outside of the **ServerObject**, for example through T-SQL. + But on instances with a large amount of audits it might be better to make + sure the **ServerObject** is recent enough, or pass in **AuditObject**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $auditObject = $sqlServerObject | Get-SqlDscAudit -Name 'MyFileAudit' + $auditObject | Disable-SqlDscAudit + + Disables the audit named **MyFileAudit**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $sqlServerObject | Disable-SqlDscAudit -Name 'MyFileAudit' + + Disables the audit named **MyFileAudit**. + + .OUTPUTS + None. +#> +function Disable-SqlDscAudit +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the rule does not yet support parsing the code when a parameter type is not available. The ScriptAnalyzer rule UseSyntacticallyCorrectExamples will always error in the editor due to https://github.com/indented-automation/Indented.ScriptAnalyzerRules/issues/8.')] + [OutputType()] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param + ( + [Parameter(ParameterSetName = 'ServerObject', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter(ParameterSetName = 'AuditObject', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Audit] + $AuditObject, + + [Parameter(ParameterSetName = 'ServerObject', Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force, + + [Parameter(ParameterSetName = 'ServerObject')] + [System.Management.Automation.SwitchParameter] + $Refresh + ) + + if ($Force.IsPresent) + { + $ConfirmPreference = 'None' + } + + if ($PSCmdlet.ParameterSetName -eq 'ServerObject') + { + $getSqlDscAuditParameters = @{ + ServerObject = $ServerObject + Name = $Name + Refresh = $Refresh + ErrorAction = 'Stop' + } + + # If this command does not find the audit it will throw an exception. + $AuditObject = Get-SqlDscAudit @getSqlDscAuditParameters + } + + $verboseDescriptionMessage = $script:localizedData.Audit_Disable_ShouldProcessVerboseDescription -f $AuditObject.Name, $AuditObject.Parent.InstanceName + $verboseWarningMessage = $script:localizedData.Audit_Disable_ShouldProcessVerboseWarning -f $AuditObject.Name + $captionMessage = $script:localizedData.Audit_Disable_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + $AuditObject.Disable() + } +} diff --git a/source/Public/Enable-SqlDscAudit.ps1 b/source/Public/Enable-SqlDscAudit.ps1 new file mode 100644 index 000000000..6e6a37a26 --- /dev/null +++ b/source/Public/Enable-SqlDscAudit.ps1 @@ -0,0 +1,94 @@ +<# + .SYNOPSIS + Enables a server audit. + + .PARAMETER ServerObject + Specifies current server connection object. + + .PARAMETER AuditObject + Specifies an audit object to enable. + + .PARAMETER Name + Specifies the name of the server audit to be enabled. + + .PARAMETER Force + Specifies that the audit should be enabled with out any confirmation. + + .PARAMETER Refresh + Specifies that the **ServerObject**'s audits should be refreshed before + trying to enable the audit object. This is helpful when audits could have + been modified outside of the **ServerObject**, for example through T-SQL. + But on instances with a large amount of audits it might be better to make + sure the **ServerObject** is recent enough, or pass in **AuditObject**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $auditObject = $sqlServerObject | Get-SqlDscAudit -Name 'MyFileAudit' + $auditObject | Enable-SqlDscAudit + + Enables the audit named **MyFileAudit**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $sqlServerObject | Enable-SqlDscAudit -Name 'MyFileAudit' + + Enables the audit named **MyFileAudit**. + + .OUTPUTS + None. +#> +function Enable-SqlDscAudit +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the rule does not yet support parsing the code when a parameter type is not available. The ScriptAnalyzer rule UseSyntacticallyCorrectExamples will always error in the editor due to https://github.com/indented-automation/Indented.ScriptAnalyzerRules/issues/8.')] + [OutputType()] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param + ( + [Parameter(ParameterSetName = 'ServerObject', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter(ParameterSetName = 'AuditObject', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Audit] + $AuditObject, + + [Parameter(ParameterSetName = 'ServerObject', Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force, + + [Parameter(ParameterSetName = 'ServerObject')] + [System.Management.Automation.SwitchParameter] + $Refresh + ) + + if ($Force.IsPresent) + { + $ConfirmPreference = 'None' + } + + if ($PSCmdlet.ParameterSetName -eq 'ServerObject') + { + $getSqlDscAuditParameters = @{ + ServerObject = $ServerObject + Name = $Name + Refresh = $Refresh + ErrorAction = 'Stop' + } + + # If this command does not find the audit it will throw an exception. + $AuditObject = Get-SqlDscAudit @getSqlDscAuditParameters + } + + $verboseDescriptionMessage = $script:localizedData.Audit_Enable_ShouldProcessVerboseDescription -f $AuditObject.Name, $AuditObject.Parent.InstanceName + $verboseWarningMessage = $script:localizedData.Audit_Enable_ShouldProcessVerboseWarning -f $AuditObject.Name + $captionMessage = $script:localizedData.Audit_Enable_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + $AuditObject.Enable() + } +} diff --git a/source/Public/Get-SqlDscAudit.ps1 b/source/Public/Get-SqlDscAudit.ps1 new file mode 100644 index 000000000..4a8943d6f --- /dev/null +++ b/source/Public/Get-SqlDscAudit.ps1 @@ -0,0 +1,70 @@ +<# + .SYNOPSIS + Get server audit. + + .PARAMETER ServerObject + Specifies current server connection object. + + .PARAMETER Name + Specifies the name of the server audit to get. + + .PARAMETER Refresh + Specifies that the **ServerObject**'s audits should be refreshed before + trying get the audit object. This is helpful when audits could have been + modified outside of the **ServerObject**, for example through T-SQL. But + on instances with a large amount of audits it might be better to make + sure the **ServerObject** is recent enough, or pass in **AuditObject**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $sqlServerObject | Get-SqlDscAudit -Name 'MyFileAudit' + + Get the audit named **MyFileAudit**. + + .OUTPUTS + `[Microsoft.SqlServer.Management.Smo.Audit]`. +#> +function Get-SqlDscAudit +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the rule does not yet support parsing the code when a parameter type is not available. The ScriptAnalyzer rule UseSyntacticallyCorrectExamples will always error in the editor due to https://github.com/indented-automation/Indented.ScriptAnalyzerRules/issues/8.')] + [OutputType([Microsoft.SqlServer.Management.Smo.Audit])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Refresh + ) + + if ($Refresh.IsPresent) + { + # Make sure the audits are up-to-date to get any newly created audits. + $ServerObject.Audits.Refresh() + } + + $auditObject = $ServerObject.Audits[$Name] + + if (-not $AuditObject) + { + $missingAuditMessage = $script:localizedData.Audit_Missing -f $Name + + $writeErrorParameters = @{ + Message = $missingAuditMessage + Category = 'InvalidOperation' + ErrorId = 'GSDA0001' # cspell: disable-line + TargetObject = $Name + } + + Write-Error @writeErrorParameters + } + + return $auditObject +} diff --git a/source/Public/Invoke-SqlDscQuery.ps1 b/source/Public/Invoke-SqlDscQuery.ps1 new file mode 100644 index 000000000..4f687e043 --- /dev/null +++ b/source/Public/Invoke-SqlDscQuery.ps1 @@ -0,0 +1,113 @@ +<# + .SYNOPSIS + Executes a query on the specified database. + + .PARAMETER ServerObject + Specifies current server connection object. + + .PARAMETER DatabaseName + Specify the name of the database to execute the query on. + + .PARAMETER Query + The query string to execute. + + .PARAMETER PassThru + Specifies if the command should return any result the query might return. + + .PARAMETER StatementTimeout + Set the query StatementTimeout in seconds. Default 600 seconds (10 minutes). + + .PARAMETER RedactText + One or more strings to redact from the query when verbose messages are + written to the console. Strings here will be escaped so they will not + be interpreted as regular expressions (RegEx). + + .OUTPUTS + `[System.Data.DataSet]` when passing parameter **PassThru**, otherwise + outputs none. + + .EXAMPLE + $serverInstance = Connect-SqlDscDatabaseEngine + Invoke-SqlDscQuery -ServerObject $serverInstance -Database master ` + -Query 'SELECT name FROM sys.databases' -PassThru + + Runs the query and returns all the database names in the instance. + + .EXAMPLE + $serverInstance = Connect-SqlDscDatabaseEngine + Invoke-SqlDscQuery -ServerObject $serverInstance -Database master ` + -Query 'RESTORE DATABASE [NorthWinds] WITH RECOVERY' + + Runs the query to restores the database NorthWinds. + + .EXAMPLE + $serverInstance = Connect-SqlDscDatabaseEngine + Invoke-SqlDscQuery -ServerObject $serverInstance -Database master ` + -Query "select * from MyTable where password = 'PlaceholderPa\ssw0rd1' and password = 'placeholder secret passphrase'" ` + -PassThru -RedactText @('PlaceholderPa\sSw0rd1','Placeholder Secret PassPhrase') ` + -Verbose + + Shows how to redact sensitive information in the query when the query string + is output as verbose information when the parameter Verbose is used. + + .NOTES + This is a wrapper for private function Invoke-Query, until it move into + this public function. +#> +function Invoke-SqlDscQuery +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the rule does not yet support parsing the code when a parameter type is not available. The ScriptAnalyzer rule UseSyntacticallyCorrectExamples will always error in the editor due to https://github.com/indented-automation/Indented.ScriptAnalyzerRules/issues/8.')] + [OutputType([System.Data.DataSet])] + # TODO: Should use ShouldProcess + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter(Mandatory = $true)] + [System.String] + $DatabaseName, + + [Parameter(Mandatory = $true)] + [System.String] + $Query, + + [Parameter()] + [Switch] + $PassThru, + + [Parameter()] + [ValidateNotNull()] + [System.Int32] + $StatementTimeout = 600, + + [Parameter()] + [System.String[]] + $RedactText + ) + + $invokeQueryParameters = @{ + SqlServerObject = $ServerObject + Database = $DatabaseName + Query = $Query + } + + if ($PSBoundParameters.ContainsKey('PassThru')) + { + $invokeQueryParameters.WithResults = $PassThru + } + + if ($PSBoundParameters.ContainsKey('StatementTimeout')) + { + $invokeQueryParameters.StatementTimeout = $StatementTimeout + } + + if ($PSBoundParameters.ContainsKey('RedactText')) + { + $invokeQueryParameters.RedactText = $RedactText + } + + return (Invoke-Query @invokeQueryParameters) +} diff --git a/source/Public/New-SqlDscAudit.ps1 b/source/Public/New-SqlDscAudit.ps1 new file mode 100644 index 000000000..1effcb200 --- /dev/null +++ b/source/Public/New-SqlDscAudit.ps1 @@ -0,0 +1,309 @@ +<# + .SYNOPSIS + Creates a server audit. + + .PARAMETER ServerObject + Specifies current server connection object. + + .PARAMETER Name + Specifies the name of the server audit to be added. + + .PARAMETER Filter + Specifies the filter that should be used on the audit. See [predicate expression](https://docs.microsoft.com/en-us/sql/t-sql/statements/create-server-audit-transact-sql) + how to write the syntax for the filter. + + .PARAMETER OnFailure + Specifies what should happen when writing events to the store fails. + This can be 'Continue', 'FailOperation', or 'Shutdown'. + + .PARAMETER QueueDelay + Specifies the maximum delay before a event is written to the store. + When set to low this could impact server performance. + When set to high events could be missing when a server crashes. + + .PARAMETER AuditGuid + Specifies the GUID found in the mirrored database. To support scenarios such + as database mirroring an audit needs a specific GUID. + + .PARAMETER Force + Specifies that the audit should be created with out any confirmation. + + .PARAMETER Refresh + Specifies that the **ServerObject**'s audits should be refreshed before + creating the audit object. This is helpful when audits could have been + modified outside of the **ServerObject**, for example through T-SQL. But + on instances with a large amount of audits it might be better to make + sure the ServerObject is recent enough. + + .PARAMETER LogType + Specifies the log location where the audit should write to. + This can be SecurityLog or ApplicationLog. + + .PARAMETER FilePath + Specifies the location where te log files wil be placed. + + .PARAMETER ReserveDiskSpace + Specifies if the needed file space should be reserved. To use this parameter + the parameter **MaximumFiles** must also be used. + + .PARAMETER MaximumFiles + Specifies the number of files on disk. + + .PARAMETER MaximumFileSize + Specifies the maximum file size in units by parameter MaximumFileSizeUnit. + + .PARAMETER MaximumFileSizeUnit + Specifies the unit that is used for the file size. This can be set to `Megabyte`, + `Gigabyte`, or `Terabyte`. + + .PARAMETER MaximumRolloverFiles + Specifies the amount of files on disk before SQL Server starts reusing + the files. If not specified then it is set to unlimited. + + .OUTPUTS + `[Microsoft.SqlServer.Management.Smo.Audit]` is passing parameter **PassThru**, + otherwise none. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $sqlServerObject | New-SqlDscAudit -Name 'MyFileAudit' -Path 'E:\auditFolder' + + Create a new file audit named **MyFileAudit**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $sqlServerObject | New-SqlDscAudit -Name 'MyAppLogAudit' -LogType 'ApplicationLog' + + Create a new application log audit named **MyAppLogAudit**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $sqlServerObject | New-SqlDscAudit -Name 'MyFileAudit' -Path 'E:\auditFolder' -PassThru + + Create a new file audit named **MyFileAudit** and returns the Audit object. + + .NOTES + This command has the confirm impact level set to medium since an audit is + created but by default is is not enabled. + + See the SQL Server documentation for more information for the possible + parameter values to pass to this command: https://docs.microsoft.com/en-us/sql/t-sql/statements/create-server-audit-transact-sql +#> +function New-SqlDscAudit +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the rule does not yet support parsing the code when a parameter type is not available. The ScriptAnalyzer rule UseSyntacticallyCorrectExamples will always error in the editor due to https://github.com/indented-automation/Indented.ScriptAnalyzerRules/issues/8.')] + [OutputType([Microsoft.SqlServer.Management.Smo.Audit])] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + param + ( + [Parameter(ParameterSetName = 'Log', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'File', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'FileWithSize', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'FileWithMaxFiles', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'FileWithMaxRolloverFiles', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'FileWithSizeAndMaxFiles', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'FileWithSizeAndMaxRolloverFiles', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.String] + $AuditFilter, + + [Parameter()] + [ValidateSet('Continue', 'FailOperation', 'Shutdown')] + [System.String] + $OnFailure, + + [Parameter()] + [ValidateRange(1000, 2147483647)] + [System.UInt32] + $QueueDelay, + + [Parameter()] + [ValidatePattern('^[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')] + [System.String] + $AuditGuid, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Refresh, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PassThru, + + [Parameter(ParameterSetName = 'Log', Mandatory = $true)] + [ValidateSet('SecurityLog', 'ApplicationLog')] + [System.String] + $LogType, + + [Parameter(ParameterSetName = 'File', Mandatory = $true)] + [Parameter(ParameterSetName = 'FileWithSize', Mandatory = $true)] + [Parameter(ParameterSetName = 'FileWithMaxFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'FileWithMaxRolloverFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'FileWithSizeAndMaxFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'FileWithSizeAndMaxRolloverFiles', Mandatory = $true)] + [ValidateScript({ + if (-not (Test-Path -Path $_)) + { + throw ($script:localizedData.Audit_PathParameterValueInvalid -f $_) + } + + return $true + })] + [System.String] + $Path, + + [Parameter(ParameterSetName = 'FileWithSize', Mandatory = $true)] + [Parameter(ParameterSetName = 'FileWithSizeAndMaxFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'FileWithSizeAndMaxRolloverFiles', Mandatory = $true)] + [ValidateRange(2, 2147483647)] + [System.UInt32] + $MaximumFileSize, + + [Parameter(ParameterSetName = 'FileWithSize', Mandatory = $true)] + [Parameter(ParameterSetName = 'FileWithSizeAndMaxFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'FileWithSizeAndMaxRolloverFiles', Mandatory = $true)] + [ValidateSet('Megabyte', 'Gigabyte', 'Terabyte')] + [System.String] + $MaximumFileSizeUnit, + + [Parameter(ParameterSetName = 'FileWithSizeAndMaxFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'FileWithMaxFiles', Mandatory = $true)] + [System.UInt32] + $MaximumFiles, + + [Parameter(ParameterSetName = 'FileWithMaxFiles')] + [Parameter(ParameterSetName = 'FileWithSizeAndMaxFiles')] + [System.Management.Automation.SwitchParameter] + $ReserveDiskSpace, + + [Parameter(ParameterSetName = 'FileWithSizeAndMaxRolloverFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'FileWithMaxRolloverFiles', Mandatory = $true)] + [ValidateRange(0, 2147483647)] + [System.UInt32] + $MaximumRolloverFiles + ) + + if ($Force.IsPresent) + { + $ConfirmPreference = 'None' + } + + $getSqlDscAuditParameters = @{ + ServerObject = $ServerObject + Name = $Name + Refresh = $Refresh + ErrorAction = 'SilentlyContinue' + } + + $auditObject = Get-SqlDscAudit @getSqlDscAuditParameters + + if ($auditObject) + { + $auditAlreadyPresentMessage = $script:localizedData.Audit_AlreadyPresent -f $Name + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + $auditAlreadyPresentMessage, + 'NSDA0001', # cspell: disable-line + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $DatabaseName + ) + ) + } + + $auditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @($ServerObject, $Name) + + $queryType = switch ($PSCmdlet.ParameterSetName) + { + 'Log' + { + $LogType + } + + default + { + 'File' + } + } + + $auditObject.DestinationType = $queryType + + if ($PSCmdlet.ParameterSetName -match 'File') + { + $auditObject.FilePath = $Path + + if ($PSCmdlet.ParameterSetName -match 'FileWithSize') + { + $convertedMaximumFileSizeUnit = ( + @{ + Megabyte = 'MB' + Gigabyte = 'GB' + Terabyte = 'TB' + } + ).$MaximumFileSizeUnit + + $auditObject.MaximumFileSize = $MaximumFileSize + $auditObject.MaximumFileSizeUnit = $convertedMaximumFileSizeUnit + } + + if ($PSCmdlet.ParameterSetName -in @('FileWithMaxFiles', 'FileWithSizeAndMaxFiles')) + { + $auditObject.MaximumFiles = $MaximumFiles + + if ($PSBoundParameters.ContainsKey('ReserveDiskSpace')) + { + $auditObject.ReserveDiskSpace = $ReserveDiskSpace.IsPresent + } + } + + if ($PSCmdlet.ParameterSetName -in @('FileWithMaxRolloverFiles', 'FileWithSizeAndMaxRolloverFiles')) + { + $auditObject.MaximumRolloverFiles = $MaximumRolloverFiles + } + } + + if ($PSBoundParameters.ContainsKey('OnFailure')) + { + $auditObject.OnFailure = $OnFailure + } + + if ($PSBoundParameters.ContainsKey('QueueDelay')) + { + $auditObject.QueueDelay = $QueueDelay + } + + if ($PSBoundParameters.ContainsKey('AuditGuid')) + { + $auditObject.Guid = $AuditGuid + } + + if ($PSBoundParameters.ContainsKey('AuditFilter')) + { + $auditObject.Filter = $AuditFilter + } + + $verboseDescriptionMessage = $script:localizedData.Audit_Add_ShouldProcessVerboseDescription -f $Name, $ServerObject.InstanceName + $verboseWarningMessage = $script:localizedData.Audit_Add_ShouldProcessVerboseWarning -f $Name + $captionMessage = $script:localizedData.Audit_Add_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + $auditObject.Create() + + if ($PassThru.IsPresent) + { + return $auditObject + } + } +} diff --git a/source/Public/Remove-SqlDscAudit.ps1 b/source/Public/Remove-SqlDscAudit.ps1 new file mode 100644 index 000000000..0500dc435 --- /dev/null +++ b/source/Public/Remove-SqlDscAudit.ps1 @@ -0,0 +1,98 @@ +<# + .SYNOPSIS + Removes a server audit. + + .PARAMETER ServerObject + Specifies current server connection object. + + .PARAMETER AuditObject + Specifies an audit object to remove. + + .PARAMETER Name + Specifies the name of the server audit to be removed. + + .PARAMETER Force + Specifies that the audit should be removed with out any confirmation. + + .PARAMETER Refresh + Specifies that the **ServerObject**'s audits should be refreshed before + trying removing the audit object. This is helpful when audits could have + been modified outside of the **ServerObject**, for example through T-SQL. + But on instances with a large amount of audits it might be better to make + sure the **ServerObject** is recent enough, or pass in **AuditObject**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $auditObject = $sqlServerObject | Get-SqlDscAudit -Name 'MyFileAudit' + $auditObject | Remove-SqlDscAudit + + Removes the audit named **MyFileAudit**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $sqlServerObject | Remove-SqlDscAudit -Name 'MyFileAudit' + + Removes the audit named **MyFileAudit**. + + .OUTPUTS + None. +#> +function Remove-SqlDscAudit +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the rule does not yet support parsing the code when a parameter type is not available. The ScriptAnalyzer rule UseSyntacticallyCorrectExamples will always error in the editor due to https://github.com/indented-automation/Indented.ScriptAnalyzerRules/issues/8.')] + [OutputType()] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param + ( + [Parameter(ParameterSetName = 'ServerObject', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter(ParameterSetName = 'AuditObject', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Audit] + $AuditObject, + + [Parameter(ParameterSetName = 'ServerObject', Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force, + + [Parameter(ParameterSetName = 'ServerObject')] + [System.Management.Automation.SwitchParameter] + $Refresh + ) + + if ($Force.IsPresent) + { + $ConfirmPreference = 'None' + } + + if ($PSCmdlet.ParameterSetName -eq 'ServerObject') + { + $getSqlDscAuditParameters = @{ + ServerObject = $ServerObject + Name = $Name + Refresh = $Refresh + ErrorAction = 'Stop' + } + + # If this command does not find the audit it will throw an exception. + $AuditObject = Get-SqlDscAudit @getSqlDscAuditParameters + } + + $verboseDescriptionMessage = $script:localizedData.Audit_Remove_ShouldProcessVerboseDescription -f $AuditObject.Name, $AuditObject.Parent.InstanceName + $verboseWarningMessage = $script:localizedData.Audit_Remove_ShouldProcessVerboseWarning -f $AuditObject.Name + $captionMessage = $script:localizedData.Audit_Remove_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + <# + If the passed audit object has already been dropped, then we silently + do nothing, using the method DropIfExist(), since the job is done. + #> + $AuditObject.DropIfExists() + } +} diff --git a/source/Public/Set-SqlDscAudit.ps1 b/source/Public/Set-SqlDscAudit.ps1 new file mode 100644 index 000000000..ff3695a52 --- /dev/null +++ b/source/Public/Set-SqlDscAudit.ps1 @@ -0,0 +1,346 @@ +<# + .SYNOPSIS + Updates a server audit. + + .PARAMETER ServerObject + Specifies current server connection object. + + .PARAMETER AuditObject + Specifies an audit object to update. + + .PARAMETER Name + Specifies the name of the server audit to be updated. + + .PARAMETER Filter + Specifies the filter that should be used on the audit. See [predicate expression](https://docs.microsoft.com/en-us/sql/t-sql/statements/create-server-audit-transact-sql) + how to write the syntax for the filter. + + .PARAMETER OnFailure + Specifies what should happen when writing events to the store fails. + This can be 'Continue', 'FailOperation', or 'Shutdown'. + + .PARAMETER QueueDelay + Specifies the maximum delay before a event is written to the store. + When set to low this could impact server performance. + When set to high events could be missing when a server crashes. + + .PARAMETER AuditGuid + Specifies the GUID found in the mirrored database. To support scenarios such + as database mirroring an audit needs a specific GUID. + + .PARAMETER Force + Specifies that the audit should be updated with out any confirmation. + + .PARAMETER Refresh + Specifies that the audit object should be refreshed before updating. This + is helpful when audits could have been modified outside of the **ServerObject**, + for example through T-SQL. But on instances with a large amount of audits + it might be better to make sure the ServerObject is recent enough. + + .PARAMETER FilePath + Specifies the location where te log files wil be placed. + + .PARAMETER ReserveDiskSpace + Specifies if the needed file space should be reserved. To use this parameter + the parameter **MaximumFiles** must also be used. + + .PARAMETER MaximumFiles + Specifies the number of files on disk. + + .PARAMETER MaximumFileSize + Specifies the maximum file size in units by parameter MaximumFileSizeUnit. + Minimum allowed value is 2 (MB). It also allowed to set the value to 0 which + mean unlimited file size. + + .PARAMETER MaximumFileSizeUnit + Specifies the unit that is used for the file size. this can be KB, MB or GB. + + .PARAMETER MaximumRolloverFiles + Specifies the amount of files on disk before SQL Server starts reusing + the files. If not specified then it is set to unlimited. + + .OUTPUTS + `[Microsoft.SqlServer.Management.Smo.Audit]` is passing parameter **PassThru**, + otherwise none. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $sqlServerObject | Set-SqlDscAudit -Name 'MyFileAudit' -Path 'E:\auditFolder' -QueueDelay 1000 + + Updates the file audit named **MyFileAudit** by setting the path to ''E:\auditFolder' + and the queue delay to 1000. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $sqlServerObject | New-SqlDscAudit -Name 'MyAppLogAudit' -QueueDelay 1000 + + Updates the application log audit named **MyAppLogAudit** by setting the + queue delay to 1000. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $sqlServerObject | Set-SqlDscAudit -Name 'MyFileAudit' -Path 'E:\auditFolder' -QueueDelay 1000 -PassThru + + Updates the file audit named **MyFileAudit** by setting the path to ''E:\auditFolder' + and the queue delay to 1000, and returns the Audit object. + + .NOTES + This command has the confirm impact level set to high since an audit is + unknown to be enable at the point when the command is issued. + + See the SQL Server documentation for more information for the possible + parameter values to pass to this command: https://docs.microsoft.com/en-us/sql/t-sql/statements/create-server-audit-transact-sql +#> +function Set-SqlDscAudit +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the rule does not yet support parsing the code when a parameter type is not available. The ScriptAnalyzer rule UseSyntacticallyCorrectExamples will always error in the editor due to https://github.com/indented-automation/Indented.ScriptAnalyzerRules/issues/8.')] + [OutputType([Microsoft.SqlServer.Management.Smo.Audit])] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param + ( + [Parameter(ParameterSetName = 'ServerObject', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithSize', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithMaxFiles', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithMaxRolloverFiles', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithSizeAndMaxFiles', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithSizeAndMaxRolloverFiles', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter(ParameterSetName = 'AuditObject', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithSize', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithMaxFiles', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithMaxRolloverFiles', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithSizeAndMaxFiles', Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithSizeAndMaxRolloverFiles', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Audit] + $AuditObject, + + [Parameter(ParameterSetName = 'ServerObject', Mandatory = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithSize', Mandatory = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithMaxFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithMaxRolloverFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithSizeAndMaxFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithSizeAndMaxRolloverFiles', Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.String] + $AuditFilter, + + [Parameter()] + [ValidateSet('Continue', 'FailOperation', 'Shutdown')] + [System.String] + $OnFailure, + + [Parameter()] + [ValidateScript({ + if ($_ -in 1..999 -or $_ -gt 2147483647) + { + throw ($script:localizedData.Audit_QueueDelayParameterValueInvalid -f $_) + } + + return $true + })] + [System.UInt32] + $QueueDelay, + + [Parameter()] + [ValidatePattern('^[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')] + [System.String] + $AuditGuid, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Refresh, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PassThru, + + [Parameter(ParameterSetName = 'ServerObject')] + [Parameter(ParameterSetName = 'ServerObjectWithSize')] + [Parameter(ParameterSetName = 'ServerObjectWithMaxFiles')] + [Parameter(ParameterSetName = 'ServerObjectWithMaxRolloverFiles')] + [Parameter(ParameterSetName = 'ServerObjectWithSizeAndMaxFiles')] + [Parameter(ParameterSetName = 'ServerObjectWithSizeAndMaxRolloverFiles')] + [Parameter(ParameterSetName = 'AuditObject')] + [Parameter(ParameterSetName = 'AuditObjectWithSize')] + [Parameter(ParameterSetName = 'AuditObjectWithMaxFiles')] + [Parameter(ParameterSetName = 'AuditObjectWithMaxRolloverFiles')] + [Parameter(ParameterSetName = 'AuditObjectWithSizeAndMaxFiles')] + [Parameter(ParameterSetName = 'AuditObjectWithSizeAndMaxRolloverFiles')] + [ValidateScript({ + if (-not (Test-Path -Path $_)) + { + throw ($script:localizedData.Audit_PathParameterValueInvalid -f $_) + } + + return $true + })] + [System.String] + $Path, + + [Parameter(ParameterSetName = 'ServerObjectWithSize', Mandatory = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithSizeAndMaxFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithSizeAndMaxRolloverFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithSize', Mandatory = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithSizeAndMaxFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithSizeAndMaxRolloverFiles', Mandatory = $true)] + [ValidateScript({ + if ($_ -eq 1 -or $_ -gt 2147483647) + { + throw ($script:localizedData.Audit_MaximumFileSizeParameterValueInvalid -f $_) + } + + return $true + })] + [System.UInt32] + $MaximumFileSize, + + [Parameter(ParameterSetName = 'ServerObjectWithSize', Mandatory = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithSizeAndMaxFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithSizeAndMaxRolloverFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithSize', Mandatory = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithSizeAndMaxFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithSizeAndMaxRolloverFiles', Mandatory = $true)] + [ValidateSet('Megabyte', 'Gigabyte', 'Terabyte')] + [System.String] + $MaximumFileSizeUnit, + + [Parameter(ParameterSetName = 'ServerObjectWithMaxFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithSizeAndMaxFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithMaxFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithSizeAndMaxFiles', Mandatory = $true)] + [System.UInt32] + $MaximumFiles, + + [Parameter(ParameterSetName = 'ServerObjectWithMaxFiles')] + [Parameter(ParameterSetName = 'ServerObjectWithSizeAndMaxFiles')] + [Parameter(ParameterSetName = 'AuditObjectWithMaxFiles')] + [Parameter(ParameterSetName = 'AuditObjectWithSizeAndMaxFiles')] + [System.Management.Automation.SwitchParameter] + $ReserveDiskSpace, + + [Parameter(ParameterSetName = 'ServerObjectWithMaxRolloverFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'ServerObjectWithSizeAndMaxRolloverFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithMaxRolloverFiles', Mandatory = $true)] + [Parameter(ParameterSetName = 'AuditObjectWithSizeAndMaxRolloverFiles', Mandatory = $true)] + [ValidateRange(0, 2147483647)] + [System.UInt32] + $MaximumRolloverFiles + ) + + if ($Force.IsPresent) + { + $ConfirmPreference = 'None' + } + + if ($PSCmdlet.ParameterSetName -eq 'ServerObject') + { + $getSqlDscAuditParameters = @{ + ServerObject = $ServerObject + Name = $Name + Refresh = $Refresh + ErrorAction = 'Stop' + } + + $AuditObject = Get-SqlDscAudit @getSqlDscAuditParameters + } + + if ($Refresh.IsPresent) + { + $AuditObject.Refresh() + } + + $verboseDescriptionMessage = $script:localizedData.Audit_Update_ShouldProcessVerboseDescription -f $AuditObject.Name, $AuditObject.Parent.InstanceName + $verboseWarningMessage = $script:localizedData.Audit_Update_ShouldProcessVerboseWarning -f $AuditObject.Name + $captionMessage = $script:localizedData.Audit_Update_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + if ($PSBoundParameters.ContainsKey('Path')) + { + $AuditObject.FilePath = $Path + } + + if ($PSCmdlet.ParameterSetName -match 'WithSize') + { + $queryMaximumFileSizeUnit = ( + @{ + Megabyte = 'MB' + Gigabyte = 'GB' + Terabyte = 'TB' + } + ).$MaximumFileSizeUnit + + $AuditObject.MaximumFileSize = $MaximumFileSize + $AuditObject.MaximumFileSizeUnit = $queryMaximumFileSizeUnit + } + + if ($PSCmdlet.ParameterSetName -match 'MaxFiles') + { + if ($AuditObject.MaximumRolloverFiles) + { + # Switching to MaximumFiles instead of MaximumRolloverFiles. + $AuditObject.MaximumRolloverFiles = 0 + + # Must run method Alter() before setting MaximumFiles. + $AuditObject.Alter() + } + + $AuditObject.MaximumFiles = $MaximumFiles + + if ($PSBoundParameters.ContainsKey('ReserveDiskSpace')) + { + $AuditObject.ReserveDiskSpace = $ReserveDiskSpace.IsPresent + } + } + + if ($PSCmdlet.ParameterSetName -match 'MaxRolloverFiles') + { + if ($AuditObject.MaximumFiles) + { + # Switching to MaximumRolloverFiles instead of MaximumFiles. + $AuditObject.MaximumFiles = 0 + + # Must run method Alter() before setting MaximumRolloverFiles. + $AuditObject.Alter() + } + + $AuditObject.MaximumRolloverFiles = $MaximumRolloverFiles + } + + if ($PSBoundParameters.ContainsKey('OnFailure')) + { + $AuditObject.OnFailure = $OnFailure + } + + if ($PSBoundParameters.ContainsKey('QueueDelay')) + { + $AuditObject.QueueDelay = $QueueDelay + } + + if ($PSBoundParameters.ContainsKey('AuditGuid')) + { + $AuditObject.Guid = $AuditGuid + } + + if ($PSBoundParameters.ContainsKey('AuditFilter')) + { + $AuditObject.Filter = $AuditFilter + } + + $AuditObject.Alter() + + if ($PassThru.IsPresent) + { + return $AuditObject + } + } +} diff --git a/source/en-US/SqlAudit.strings.psd1 b/source/en-US/SqlAudit.strings.psd1 new file mode 100644 index 000000000..8a818d13b --- /dev/null +++ b/source/en-US/SqlAudit.strings.psd1 @@ -0,0 +1,21 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource SqlPermission. +#> + +ConvertFrom-StringData @' + ## Strings overrides for the ResourceBase's default strings. + # None + + ## Strings directly used by the derived class SqlDatabasePermission. + BothFileSizePropertiesMustBeSet = Both the parameter MaximumFileSize and MaximumFileSizeUnit must be assigned. (SA0001) + ReservDiskSpaceWithoutMaximumFiles = The parameter ReservDiskSpace can only be used together with the parameter MaximumFiles. (SA0002) + PathInvalid = The path '{0}' does not exist. Audit file can only be created in a path that already exist and where the SQL Server instance has permission to write. (SA0003) + EvaluateServerAudit = Evaluate the current audit '{0}' on the instance '{1}'. (SA0004) + MaximumFileSizeValueInvalid = The maximum file size must be set to a value of 0 or a value between 2 and 2147483647. (SA0004) + QueueDelayValueInvalid = The queue delay must be set to a value of 0 or a value between 1000 and 2147483647. (SA0005) + CannotCreateNewAudit = Cannot create a new audit because neither of the properties LogType or Path is specified. One of those properties must be specified to create a new audit. (SA0006) + AuditOfWrongTypeForUseWithProperty = A property that is not in desired state is not compatible with the audit type '{0}'. (SA0007) + AuditIsWrongType = The existing audit is of wrong type to be able to update the property that is not in desired state. If the audit should be re-created set Force to $true. (SA0008) +'@ diff --git a/source/en-US/SqlResourceBase.strings.psd1 b/source/en-US/SqlResourceBase.strings.psd1 new file mode 100644 index 000000000..6c943bb14 --- /dev/null +++ b/source/en-US/SqlResourceBase.strings.psd1 @@ -0,0 +1,8 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + class SqlResourceBase. +#> + +ConvertFrom-StringData @' +'@ diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index 734b3a377..45ae2660b 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -26,7 +26,7 @@ ConvertFrom-StringData @' ## Get-SqlDscServerPermission, Set-SqlDscServerPermission ServerPermission_MissingPrincipal = The principal '{0}' is not a login on the instance '{1}'. - ## Set-SqlDscDatabasePermission + ## Set-SqlDscServerPermission ServerPermission_IgnoreWithGrantForStateDeny = The parameter WithGrant cannot be used together with the state Deny, the parameter WithGrant is ignored. ServerPermission_ChangePermissionShouldProcessVerboseDescription = Changing the permission for the principal '{0}' on the instance '{1}'. ServerPermission_ChangePermissionShouldProcessVerboseWarning = Are you sure you want you change the permission for the principal '{0}'? @@ -38,4 +38,43 @@ ConvertFrom-StringData @' ## Class DatabasePermission InvalidTypeForCompare = Invalid type in comparison. Expected type [{0}], but the type was [{1}]. (DP0001) + + ## New-SqlDscAudit, Set-SqlDscAudit + Audit_PathParameterValueInvalid = The path '{0}' does not exist. Audit file can only be created in a path that already exist and where the SQL Server instance has permission to write. + + ## New-SqlDscAudit + Audit_Add_ShouldProcessVerboseDescription = Adding the audit '{0}' on the instance '{1}'. + Audit_Add_ShouldProcessVerboseWarning = Are you sure you want you add the audit '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Audit_Add_ShouldProcessCaption = Add audit on instance + Audit_AlreadyPresent = There is already an audit with the name '{0}'. + + ## Set-SqlDscAudit + Audit_Update_ShouldProcessVerboseDescription = Updating the audit '{0}' on the instance '{1}'. + Audit_Update_ShouldProcessVerboseWarning = Are you sure you want you update the audit '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Audit_Update_ShouldProcessCaption = Update audit on instance + Audit_MaximumFileSizeParameterValueInvalid = The maximum file size must be set to a value of 0 or a value between 2 and 2147483647. + Audit_QueueDelayParameterValueInvalid = The queue delay must be set to a value of 0 or a value between 1000 and 2147483647. + + ## Get-SqlDscAudit + Audit_Missing = There is no audit with the name '{0}'. + + ## Remove-SqlDscAudit + Audit_Remove_ShouldProcessVerboseDescription = Removing the audit '{0}' on the instance '{1}'. + Audit_Remove_ShouldProcessVerboseWarning = Are you sure you want you remove the audit '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Audit_Remove_ShouldProcessCaption = Remove audit on instance + + ## Enable-SqlDscAudit + Audit_Enable_ShouldProcessVerboseDescription = Enabling the audit '{0}' on the instance '{1}'. + Audit_Enable_ShouldProcessVerboseWarning = Are you sure you want you enable the audit '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Audit_Enable_ShouldProcessCaption = Enable audit on instance + + ## Disable-SqlDscAudit + Audit_Disable_ShouldProcessVerboseDescription = Disabling the audit '{0}' on the instance '{1}'. + Audit_Disable_ShouldProcessVerboseWarning = Are you sure you want you disable the audit '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Audit_Disable_ShouldProcessCaption = Disable audit on instance '@ diff --git a/tests/Integration/DSC_SqlAudit.Integration.Tests.ps1 b/tests/Integration/DSC_SqlAudit.Integration.Tests.ps1 new file mode 100644 index 000000000..1a8801404 --- /dev/null +++ b/tests/Integration/DSC_SqlAudit.Integration.Tests.ps1 @@ -0,0 +1,377 @@ +BeforeDiscovery { + try + { + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -Tasks build" first.' + } + + <# + Need to define that variables here to be used in the Pester Discover to + build the ForEach-blocks. + #> + $script:dscResourceFriendlyName = 'SqlAudit' + $script:dscResourceName = "DSC_$($script:dscResourceFriendlyName)" +} + +BeforeAll { + # Need to define the variables here which will be used in Pester Run. + $script:dscModuleName = 'SqlServerDsc' + $script:dscResourceFriendlyName = 'SqlAudit' + $script:dscResourceName = "DSC_$($script:dscResourceFriendlyName)" + + $script:testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType 'Integration' + + $configFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:dscResourceName).config.ps1" + . $configFile +} + +AfterAll { + Restore-TestEnvironment -TestEnvironment $script:testEnvironment +} + +Describe "$($script:dscResourceName)_Integration" -Tag @('Integration_SQL2016', 'Integration_SQL2017', 'Integration_SQL2019') { + BeforeAll { + $resourceId = "[$($script:dscResourceFriendlyName)]Integration_Test" + } + + Context ('When using configuration <_>') -ForEach @( + "$($script:dscResourceName)_Prerequisites_Config" + ) { + BeforeAll { + $configurationName = $_ + } + + AfterAll { + Wait-ForIdleLcm + } + + It 'Should compile and apply the MOF without throwing' { + { + $configurationParameters = @{ + OutputPath = $TestDrive + # The variable $ConfigurationData was dot-sourced above. + ConfigurationData = $ConfigurationData + } + + & $configurationName @configurationParameters + + $startDscConfigurationParameters = @{ + Path = $TestDrive + ComputerName = 'localhost' + Wait = $true + Verbose = $true + Force = $true + ErrorAction = 'Stop' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + } + + Context ('When using configuration <_>') -ForEach @( + "$($script:dscResourceName)_AddFileAudit_Config" + ) { + BeforeAll { + $configurationName = $_ + } + + AfterAll { + Wait-ForIdleLcm + } + + It 'Should compile and apply the MOF without throwing' { + { + $configurationParameters = @{ + OutputPath = $TestDrive + # The variable $ConfigurationData was dot-sourced above. + ConfigurationData = $ConfigurationData + } + + & $configurationName @configurationParameters + + $startDscConfigurationParameters = @{ + Path = $TestDrive + ComputerName = 'localhost' + Wait = $true + Verbose = $true + Force = $true + ErrorAction = 'Stop' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.Ensure | Should -Be 'Present' + $resourceCurrentState.Name | Should -Be $ConfigurationData.AllNodes.AuditName1 + $resourceCurrentState.ServerName | Should -Be $ConfigurationData.AllNodes.ServerName + $resourceCurrentState.InstanceName | Should -Be $ConfigurationData.AllNodes.InstanceName + $resourceCurrentState.Path | Should -Be $ConfigurationData.AllNodes.Path1 + $resourceCurrentState.MaximumFileSize | Should -Be $ConfigurationData.AllNodes.MaximumFileSize1 + $resourceCurrentState.MaximumFileSizeUnit | Should -Be $ConfigurationData.AllNodes.MaximumFileSizeUnit1 + $resourceCurrentState.MaximumRolloverFiles | Should -Be $ConfigurationData.AllNodes.MaximumRolloverFiles1 + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + Context ('When using configuration <_>') -ForEach @( + "$($script:dscResourceName)_AddSecLogAudit_Config" + ) { + BeforeAll { + $configurationName = $_ + } + + AfterAll { + Wait-ForIdleLcm + } + + It 'Should compile and apply the MOF without throwing' { + { + $configurationParameters = @{ + OutputPath = $TestDrive + # The variable $ConfigurationData was dot-sourced above. + ConfigurationData = $ConfigurationData + } + + & $configurationName @configurationParameters + + $startDscConfigurationParameters = @{ + Path = $TestDrive + ComputerName = 'localhost' + Wait = $true + Verbose = $true + Force = $true + ErrorAction = 'Stop' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.Ensure | Should -Be 'Present' + $resourceCurrentState.Name | Should -Be $ConfigurationData.AllNodes.AuditName2 + $resourceCurrentState.ServerName | Should -Be $ConfigurationData.AllNodes.ServerName + $resourceCurrentState.InstanceName | Should -Be $ConfigurationData.AllNodes.InstanceName + $resourceCurrentState.LogType | Should -Be $ConfigurationData.AllNodes.LogType2 + $resourceCurrentState.AuditFilter | Should -Be $ConfigurationData.AllNodes.AuditFilter2 + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + Context ('When using configuration <_>') -ForEach @( + "$($script:dscResourceName)_AddSecLogAuditNoFilter_Config" + ) { + BeforeAll { + $configurationName = $_ + } + + AfterAll { + Wait-ForIdleLcm + } + + It 'Should compile and apply the MOF without throwing' { + { + $configurationParameters = @{ + OutputPath = $TestDrive + # The variable $ConfigurationData was dot-sourced above. + ConfigurationData = $ConfigurationData + } + + & $configurationName @configurationParameters + + $startDscConfigurationParameters = @{ + Path = $TestDrive + ComputerName = 'localhost' + Wait = $true + Verbose = $true + Force = $true + ErrorAction = 'Stop' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.Ensure | Should -Be 'Present' + $resourceCurrentState.Name | Should -Be $ConfigurationData.AllNodes.AuditName2 + $resourceCurrentState.ServerName | Should -Be $ConfigurationData.AllNodes.ServerName + $resourceCurrentState.InstanceName | Should -Be $ConfigurationData.AllNodes.InstanceName + $resourceCurrentState.LogType | Should -Be $ConfigurationData.AllNodes.LogType2 + $resourceCurrentState.AuditFilter | Should -BeNullOrEmpty + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + Context ('When using configuration <_>') -ForEach @( + "$($script:dscResourceName)_RemoveAudit1_Config" + ) { + BeforeAll { + $configurationName = $_ + } + + AfterAll { + Wait-ForIdleLcm + } + + It 'Should compile and apply the MOF without throwing' { + { + $configurationParameters = @{ + OutputPath = $TestDrive + # The variable $ConfigurationData was dot-sourced above. + ConfigurationData = $ConfigurationData + } + + & $configurationName @configurationParameters + + $startDscConfigurationParameters = @{ + Path = $TestDrive + ComputerName = 'localhost' + Wait = $true + Verbose = $true + Force = $true + ErrorAction = 'Stop' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.Ensure | Should -Be 'Absent' + $resourceCurrentState.Name | Should -Be $ConfigurationData.AllNodes.AuditName1 + $resourceCurrentState.ServerName | Should -Be $ConfigurationData.AllNodes.ServerName + $resourceCurrentState.InstanceName | Should -Be $ConfigurationData.AllNodes.InstanceName + $resourceCurrentState.Path | Should -BeNullOrEmpty + $resourceCurrentState.MaximumFileSize | Should -BeNullOrEmpty + $resourceCurrentState.MaximumFileSizeUnit | Should -BeNullOrEmpty + $resourceCurrentState.MaximumRolloverFiles | Should -BeNullOrEmpty + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + Context ('When using configuration <_>') -ForEach @( + "$($script:dscResourceName)_RemoveSecLogAudit_Config" + ) { + BeforeAll { + $configurationName = $_ + } + + AfterAll { + Wait-ForIdleLcm + } + + It 'Should compile and apply the MOF without throwing' { + { + $configurationParameters = @{ + OutputPath = $TestDrive + # The variable $ConfigurationData was dot-sourced above. + ConfigurationData = $ConfigurationData + } + + & $configurationName @configurationParameters + + $startDscConfigurationParameters = @{ + Path = $TestDrive + ComputerName = 'localhost' + Wait = $true + Verbose = $true + Force = $true + ErrorAction = 'Stop' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.Ensure | Should -Be 'Absent' + $resourceCurrentState.Name | Should -Be $ConfigurationData.AllNodes.AuditName2 + $resourceCurrentState.ServerName | Should -Be $ConfigurationData.AllNodes.ServerName + $resourceCurrentState.InstanceName | Should -Be $ConfigurationData.AllNodes.InstanceName + $resourceCurrentState.LogType | Should -BeNullOrEmpty + $resourceCurrentState.AuditFilter | Should -BeNullOrEmpty + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } +} diff --git a/tests/Integration/DSC_SqlAudit.config.ps1 b/tests/Integration/DSC_SqlAudit.config.ps1 new file mode 100644 index 000000000..6c650220c --- /dev/null +++ b/tests/Integration/DSC_SqlAudit.config.ps1 @@ -0,0 +1,192 @@ +#region HEADER +# Integration Test Config Template Version: 1.2.0 +#endregion + +$configFile = [System.IO.Path]::ChangeExtension($MyInvocation.MyCommand.Path, 'json') +if (Test-Path -Path $configFile) +{ + <# + Allows reading the configuration data from a JSON file, + for real testing scenarios outside of the CI. + #> + $ConfigurationData = Get-Content -Path $configFile | ConvertFrom-Json +} +else +{ + $ConfigurationData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + CertificateFile = $env:DscPublicCertificatePath + + <# + This must be either the UPN username (e.g. username@domain.local) + or the user name without the NetBIOS name (e.g. username). Using + the NetBIOS name (e.g. DOMAIN\username) will not work. + #> + UserName = "SqlAdmin" + Password = 'P@ssw0rd1' + + ServerName = $env:COMPUTERNAME + InstanceName = 'DSCSQLTEST' + + AuditName1 = 'FileAudit' + Path1 = 'C:\Temp\audit' + MaximumFileSize1 = 10 + MaximumFileSizeUnit1 = 'Megabyte' + MaximumRolloverFiles1 = 11 + + AuditName2 = 'SecLogAudit' + LogType2 = 'SecurityLog' + AuditFilter2 = '([server_principal_name] like ''%ADMINISTRATOR'')' + } + ) + } +} + +<# + .SYNOPSIS + Creates a folder that is needed for creating a File audit. +#> +Configuration DSC_SqlAudit_Prerequisites_Config +{ + Import-DscResource -ModuleName 'SqlServerDsc' + + node $AllNodes.NodeName + { + File 'Integration_Test' + { + Ensure = 'Present' + DestinationPath = $Node.Path1 + Type = 'Directory' + Force = $true + } + } +} + +<# + .SYNOPSIS + Creates a Server Audit with File destination. +#> +Configuration DSC_SqlAudit_AddFileAudit_Config +{ + Import-DscResource -ModuleName 'SqlServerDsc' + + node $AllNodes.NodeName + { + SqlAudit 'Integration_Test' + { + Ensure = 'Present' + ServerName = $Node.ServerName + InstanceName = $Node.InstanceName + Name = $Node.AuditName1 + Path = $Node.Path1 + MaximumFileSize = $Node.MaximumFileSize1 + MaximumFileSizeUnit = $Node.MaximumFileSizeUnit1 + MaximumRolloverFiles = $Node.MaximumRolloverFiles1 + + Credential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @($Node.Username, (ConvertTo-SecureString -String $Node.Password -AsPlainText -Force)) + } + } +} + +<# + .SYNOPSIS + Creates a audit to the security log, with a filter. +#> +Configuration DSC_SqlAudit_AddSecLogAudit_Config +{ + Import-DscResource -ModuleName 'SqlServerDsc' + + node $AllNodes.NodeName + { + SqlAudit 'Integration_Test' + { + Ensure = 'Present' + ServerName = $Node.ServerName + InstanceName = $Node.InstanceName + Name = $Node.AuditName2 + LogType = $Node.LogType2 + AuditFilter = $Node.AuditFilter2 + + Credential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @($Node.Username, (ConvertTo-SecureString -String $Node.Password -AsPlainText -Force)) + } + } +} + +<# + .SYNOPSIS + Should remove the filter +#> +Configuration DSC_SqlAudit_AddSecLogAuditNoFilter_Config +{ + Import-DscResource -ModuleName 'SqlServerDsc' + + node $AllNodes.NodeName + { + SqlAudit 'Integration_Test' + { + Ensure = 'Present' + ServerName = $Node.ServerName + InstanceName = $Node.InstanceName + Name = $Node.AuditName2 + AuditFilter = '' + + Credential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @($Node.Username, (ConvertTo-SecureString -String $Node.Password -AsPlainText -Force)) + } + } +} + +<# + .SYNOPSIS + Removes the file audit. +#> +Configuration DSC_SqlAudit_RemoveAudit1_Config +{ + Import-DscResource -ModuleName 'SqlServerDsc' + + node $AllNodes.NodeName + { + SqlAudit 'Integration_Test' + { + Ensure = 'Absent' + ServerName = $Node.ServerName + InstanceName = $Node.InstanceName + Name = $Node.AuditName1 + + Credential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @($Node.Username, (ConvertTo-SecureString -String $Node.Password -AsPlainText -Force)) + } + } +} + +<# + .SYNOPSIS + Removes the log audit. +#> +Configuration DSC_SqlAudit_RemoveSecLogAudit_Config +{ + Import-DscResource -ModuleName 'SqlServerDsc' + + node $AllNodes.NodeName + { + SqlAudit 'Integration_Test' + { + Ensure = 'Absent' + ServerName = $Node.ServerName + InstanceName = $Node.InstanceName + Name = $Node.AuditName2 + + Credential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @($Node.Username, (ConvertTo-SecureString -String $Node.Password -AsPlainText -Force)) + } + } +} diff --git a/tests/Integration/README.md b/tests/Integration/README.md index 7ec990759..d6bbe1cba 100644 --- a/tests/Integration/README.md +++ b/tests/Integration/README.md @@ -444,6 +444,22 @@ and the named instance `DSCSQLTEST` have the feature `REPLICATION` installed. The integration test will not leave anything on any instance. +### SqlAudit + +**Run order:** 3 + +**Depends on:** SqlSetup + +This integration tests depends on the named instance `DSCSQLTEST`. + +The integration test will not leave anything on any instance. + +The integration tests will leave a created path on the filesystem: + +Path | +--- | +C\Temp\audit | + ### SqlScript **Run order:** 4 diff --git a/tests/Unit/Classes/ResourceBase.Tests.ps1 b/tests/Unit/Classes/ResourceBase.Tests.ps1 index 9a3379816..bbc6f5c18 100644 --- a/tests/Unit/Classes/ResourceBase.Tests.ps1 +++ b/tests/Unit/Classes/ResourceBase.Tests.ps1 @@ -101,7 +101,7 @@ Describe 'ResourceBase\Get()' -Tag 'Get' { } } - Context 'When the configuration should be present' { + Context 'When the object should be Present' { BeforeAll { Mock -CommandName Get-ClassName -MockWith { # Only return localized strings for this class name. @@ -153,6 +153,7 @@ class MyMockResource : ResourceBase the base class' method Get() return that value. #> return @{ + MyResourceKeyProperty1 = 'MyValue1' MyResourceProperty2 = 'MyValue2' } } @@ -186,7 +187,7 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() } } - Context 'When the configuration should be absent' { + Context 'When the object should be Absent' { BeforeAll { Mock -CommandName Get-ClassName -MockWith { # Only return localized strings for this class name. @@ -226,13 +227,12 @@ class MyMockResource : ResourceBase MyMockResource() : base () { # Test not to add the key property to the list of properties that are not enforced. - $this.notEnforcedProperties = @() + $this.notEnforcedProperties = @('MyResourceKeyProperty1') } [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) { return @{ - MyResourceKeyProperty1 = $null MyResourceProperty2 = $null } } @@ -258,12 +258,10 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() $getResult = $mockResourceBaseInstance.Get() - $getResult.MyResourceKeyProperty1 | Should -BeNullOrEmpty + $getResult.MyResourceKeyProperty1 | Should -Be 'MyValue1' $getResult.MyResourceProperty2 | Should -BeNullOrEmpty $getResult.Ensure | Should -Be ([Ensure]::Absent) - - $getResult.Reasons | Should -HaveCount 1 - $getResult.Reasons[0].Code | Should -Be 'MyMockResource:MyMockResource:MyResourceKeyProperty1' + $getResult.Reasons | Should -BeNullOrEmpty } } } @@ -450,6 +448,12 @@ class MyMockResource : ResourceBase [Reason[]] $Reasons + MyMockResource() : base () + { + # Test not to add the key property to the list of properties that are not enforced. + $this.notEnforcedProperties = @('MyResourceKeyProperty1') + } + [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) { return @{ @@ -481,7 +485,7 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() $getResult.MyResourceKeyProperty1 | Should -Be 'MyValue1' $getResult.MyResourceProperty2 | Should -Be 'MyValue2' - $getResult.Ensure | Should -Be ([Ensure]::Absent) + $getResult.Ensure | Should -Be ([Ensure]::Present) $getResult.Reasons | Should -HaveCount 1 $getResult.Reasons[0].Code | Should -Be 'MyMockResource:MyMockResource:MyResourceProperty2' @@ -490,7 +494,7 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() } } - Context 'When a mandatory parameter is not in desired state' { + Context 'When the object should be Present' { BeforeAll { <# Must use a here-string because we need to pass 'using' which must be @@ -518,10 +522,15 @@ class MyMockResource : ResourceBase [Reason[]] $Reasons + MyMockResource() : base () + { + # Test not to add the key property to the list of properties that are not enforced. + $this.notEnforcedProperties = @('MyResourceKeyProperty1') + } + [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) { return @{ - MyResourceKeyProperty1 = $null } } } @@ -545,18 +554,18 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() $getResult = $mockResourceBaseInstance.Get() - $getResult.MyResourceKeyProperty1 | Should -BeNullOrEmpty + $getResult.MyResourceKeyProperty1 | Should -Be 'MyValue1' $getResult.Ensure | Should -Be ([Ensure]::Absent) $getResult.Reasons | Should -HaveCount 1 - $getResult.Reasons[0].Code | Should -Be 'MyMockResource:MyMockResource:MyResourceKeyProperty1' - $getResult.Reasons[0].Phrase | Should -Be 'The property MyResourceKeyProperty1 should be "MyValue1", but was null' + $getResult.Reasons[0].Code | Should -Be 'MyMockResource:MyMockResource:Ensure' + $getResult.Reasons[0].Phrase | Should -Be 'The property Ensure should be "Present", but was "Absent"' } } } } - Context 'When the configuration should be absent' { + Context 'When the object should be Absent' { BeforeAll { Mock -CommandName Get-ClassName -MockWith { # Only return localized strings for this class name. @@ -589,6 +598,12 @@ class MyMockResource : ResourceBase [Reason[]] $Reasons + MyMockResource() : base () + { + # Test not to add the key property to the list of properties that are not enforced. + $this.notEnforcedProperties = @('MyResourceKeyProperty1') + } + [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) { return @{ @@ -622,7 +637,9 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() $getResult.MyResourceProperty2 | Should -Be 'MyValue2' $getResult.Ensure | Should -Be ([Ensure]::Present) - $getResult.Reasons | Should -BeNullOrEmpty + $getResult.Reasons | Should -HaveCount 1 + $getResult.Reasons[0].Code | Should -Be 'MyMockResource:MyMockResource:Ensure' + $getResult.Reasons[0].Phrase | Should -Be 'The property Ensure should be "Absent", but was "Present"' } } } diff --git a/tests/Unit/Classes/SqlAudit.Tests.ps1 b/tests/Unit/Classes/SqlAudit.Tests.ps1 new file mode 100644 index 000000000..c9cfa02d9 --- /dev/null +++ b/tests/Unit/Classes/SqlAudit.Tests.ps1 @@ -0,0 +1,1507 @@ +<# + .SYNOPSIS + Unit test for DSC_SqlAudit DSC resource. +#> + +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../../TestHelpers/CommonTestHelper.psm1') + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + # Load the correct SQL Module stub + $script:stubModuleName = Import-SQLModuleStub -PassThru + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force + + # Unload the stub module. + Remove-SqlModuleStub -Name $script:stubModuleName + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlAudit' { + Context 'When class is instantiated' { + It 'Should not throw an exception' { + InModuleScope -ScriptBlock { + { [SqlAudit]::new() } | Should -Not -Throw + } + } + + It 'Should have a default or empty constructor' { + InModuleScope -ScriptBlock { + $instance = [SqlAudit]::new() + $instance | Should -Not -BeNullOrEmpty + } + } + + It 'Should be the correct type' { + InModuleScope -ScriptBlock { + $instance = [SqlAudit]::new() + $instance.GetType().Name | Should -Be 'SqlAudit' + } + } + } +} + +Describe 'SqlAudit\Get()' -Tag 'Get' { + Context 'When the system is in the desired state' { + Context 'When having a File audit with default values' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } + + <# + This mocks the method GetCurrentState(). + + Method Get() will call the base method Get() which will + call back to the derived class method GetCurrentState() + to get the result to return from the derived method Get(). + #> + $script:mockSqlAuditInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { + return [System.Collections.Hashtable] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should return the correct values' { + InModuleScope -ScriptBlock { + $currentState = $script:mockSqlAuditInstance.Get() + + $currentState.InstanceName | Should -Be 'NamedInstance' + $currentState.Name | Should -Be 'MockAuditName' + $currentState.ServerName | Should -Be (Get-ComputerName) + $currentState.Credential | Should -BeNullOrEmpty + $currentState.Reasons | Should -BeNullOrEmpty + + $currentState.Path | Should -Be 'C:\Temp' + } + } + + Context 'When using parameter Credential' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance.Credential = [System.Management.Automation.PSCredential]::new( + 'MyCredentialUserName', + [SecureString]::new() + ) + + <# + This mocks the method GetCurrentState(). + + Method Get() will call the base method Get() which will + call back to the derived class method GetCurrentState() + to get the result to return from the derived method Get(). + #> + $script:mockSqlAuditInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { + return [System.Collections.Hashtable] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + Credential = $this.Credential + } + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should return the correct values' { + InModuleScope -ScriptBlock { + $currentState = $script:mockSqlAuditInstance.Get() + + $currentState.InstanceName | Should -Be 'NamedInstance' + $currentState.Name | Should -Be 'MockAuditName' + $currentState.ServerName | Should -Be (Get-ComputerName) + $currentState.Reasons | Should -BeNullOrEmpty + + $currentState.Credential | Should -BeOfType [System.Management.Automation.PSCredential] + $currentState.Credential.UserName | Should -Be 'MyCredentialUserName' + + $currentState.Path | Should -Be 'C:\Temp' + } + } + } + } + } + + Context 'When the system is not in the desired state' { + Context 'When property Path have the wrong value for a File audit' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\NewFolder' + } + + <# + This mocks the method GetCurrentState(). + + Method Get() will call the base method Get() which will + call back to the derived class method GetCurrentState() + to get the result to return from the derived method Get(). + #> + $script:mockSqlAuditInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { + return [System.Collections.Hashtable] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should return the correct values' { + InModuleScope -ScriptBlock { + $currentState = $script:mockSqlAuditInstance.Get() + + $currentState.InstanceName | Should -Be 'NamedInstance' + $currentState.Name | Should -Be 'MockAuditName' + $currentState.ServerName | Should -Be (Get-ComputerName) + $currentState.Credential | Should -BeNullOrEmpty + + $currentState.Path | Should -Be 'C:\Temp' + + $currentState.Reasons | Should -HaveCount 1 + $currentState.Reasons[0].Code | Should -Be 'SqlAudit:SqlAudit:Path' + $currentState.Reasons[0].Phrase | Should -Be 'The property Path should be "C:\NewFolder", but was "C:\Temp"' + } + } + } + } +} + +Describe 'SqlAudit\Set()' -Tag 'Set' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } | + # Mock method Modify which is called by the base method Set(). + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Modify' -Value { + $script:mockMethodModifyCallCount += 1 + } -PassThru + } + } + + BeforeEach { + InModuleScope -ScriptBlock { + $script:mockMethodModifyCallCount = 0 + } + } + + Context 'When the system is in the desired state' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance | + # Mock method Compare() which is called by the base method Set() + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Compare' -Value { + return $null + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should not call method Modify()' { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance.Set() + + $script:mockMethodModifyCallCount | Should -Be 0 + } + } + } + + Context 'When the system is not in the desired state' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance | + # Mock method Compare() which is called by the base method Set() + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Compare' -Value { + return @{ + Property = 'Path' + ExpectedValue = 'C:\NewFolder' + ActualValue = 'C:\Path' + } + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should not call method Modify()' { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance.Set() + + $script:mockMethodModifyCallCount | Should -Be 1 + } + } + } +} + +Describe 'SqlAudit\Test()' -Tag 'Test' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } + } + } + + Context 'When the system is in the desired state' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance | + # Mock method Compare() which is called by the base method Set() + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Compare' -Value { + return $null + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should return $true' { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance.Test() | Should -BeTrue + } + } + } + + Context 'When the system is not in the desired state' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance | + # Mock method Compare() which is called by the base method Set() + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Compare' -Value { + return @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\WrongFolder' + } + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should return $false' { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance.Test() | Should -BeFalse + } + } + } +} + +Describe 'SqlAudit\GetCurrentState()' -Tag 'GetCurrentState' { + Context 'When audit is missing in the current state' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru + } + + Mock -CommandName Get-SqlDscAudit + } + + It 'Should return the correct values' { + InModuleScope -ScriptBlock { + $currentState = $script:mockSqlAuditInstance.GetCurrentState( + @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + } + ) + + $currentState.InstanceName | Should -Be 'NamedInstance' + $currentState.ServerName | Should -Be (Get-ComputerName) + $currentState.Force | Should -BeFalse + $currentState.Credential | Should -BeNullOrEmpty + } + } + + Context 'When using property Credential' { + It 'Should return the correct values' { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance.Credential = [System.Management.Automation.PSCredential]::new( + 'MyCredentialUserName', + [SecureString]::new() + ) + + $currentState = $script:mockSqlAuditInstance.GetCurrentState( + @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + } + ) + + $currentState.InstanceName | Should -Be 'NamedInstance' + $currentState.ServerName | Should -Be (Get-ComputerName) + $currentState.Force | Should -BeFalse + + $currentState.Credential | Should -BeOfType [System.Management.Automation.PSCredential] + $currentState.Credential.UserName | Should -Be 'MyCredentialUserName' + } + } + } + } + + Context 'When the audit is present in the current state' { + Context 'When the audit is of type file' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru + } + + Mock -CommandName Get-SqlDscAudit -MockWith { + $mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'), + 'MockAuditName' + ) + + <# + All file properties is set to a value in this test regardless + that they can not all be set at the same time in a real scenario, + e.g. MaximumFiles and MaximumRolloverFiles that are not allowed + to be set to a non-zero value at the same time. + #> + $mockAuditObject.DestinationType = 'File' + $mockAuditObject.FilePath = 'C:\Temp' + $mockAuditObject.Filter = '([server_principal_name] like ''%ADMINISTRATOR'')' + $mockAuditObject.MaximumFiles = 2 + $mockAuditObject.MaximumFileSize = 2 + $mockAuditObject.MaximumFileSizeUnit = [Microsoft.SqlServer.Management.Smo.AuditFileSizeUnit]::Mb + $mockAuditObject.MaximumRolloverFiles = 2 + $mockAuditObject.OnFailure = 'Continue' + $mockAuditObject.QueueDelay = 1000 + $mockAuditObject.Guid = '06962963-ddd1-4a6b-86d6-0ef8d99b8e7b' + $mockAuditObject.ReserveDiskSpace = $true + $mockAuditObject.Enabled = $true + + return $mockAuditObject + } + } + + It 'Should return the correct values' { + InModuleScope -ScriptBlock { + $currentState = $script:mockSqlAuditInstance.GetCurrentState( + @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + } + ) + + $currentState.InstanceName | Should -Be 'NamedInstance' + $currentState.ServerName | Should -Be (Get-ComputerName) + $currentState.Force | Should -BeFalse + $currentState.Credential | Should -BeNullOrEmpty + + $currentState.LogType | Should -BeNullOrEmpty + $currentState.Path | Should -Be 'C:\Temp' + $currentState.AuditFilter | Should -Be '([server_principal_name] like ''%ADMINISTRATOR'')' + $currentState.MaximumFiles | Should -Be 2 + $currentState.MaximumFileSize | Should -Be 2 + $currentState.MaximumFileSizeUnit | Should -Be 'Megabyte' + $currentState.MaximumRolloverFiles | Should -Be 2 + $currentState.ReserveDiskSpace | Should -BeTrue + $currentState.OnFailure | Should -Be 'Continue' + $currentState.QueueDelay | Should -Be 1000 + $currentState.AuditGuid | Should -Be '06962963-ddd1-4a6b-86d6-0ef8d99b8e7b' + $currentState.Enabled | Should -BeTrue + } + } + } + + Context 'When the audit is of type log' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru + } + + Mock -CommandName Get-SqlDscAudit -MockWith { + $mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'), + 'MockAuditName' + ) + + <# + All file properties is set to a value in this test regardless + that they can not all be set at the same time in a real scenario, + e.g. MaximumFiles and MaximumRolloverFiles that are not allowed + to be set to a non-zero value at the same time. + #> + $mockAuditObject.DestinationType = 'SecurityLog' + $mockAuditObject.Filter = '([server_principal_name] like ''%ADMINISTRATOR'')' + $mockAuditObject.OnFailure = 'Continue' + $mockAuditObject.QueueDelay = 1000 + $mockAuditObject.Guid = '06962963-ddd1-4a6b-86d6-0ef8d99b8e7b' + $mockAuditObject.Enabled = $true + + return $mockAuditObject + } + } + + It 'Should return the correct values' { + InModuleScope -ScriptBlock { + $currentState = $script:mockSqlAuditInstance.GetCurrentState( + @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + } + ) + + $currentState.InstanceName | Should -Be 'NamedInstance' + $currentState.ServerName | Should -Be (Get-ComputerName) + $currentState.Force | Should -BeFalse + $currentState.Credential | Should -BeNullOrEmpty + + $currentState.LogType | Should -Be 'SecurityLog' + $currentState.Path | Should -BeNullOrEmpty + $currentState.AuditFilter | Should -Be '([server_principal_name] like ''%ADMINISTRATOR'')' + $currentState.MaximumFiles | Should -Be 0 + $currentState.MaximumFileSize | Should -Be 0 + $currentState.MaximumFileSizeUnit | Should -BeNullOrEmpty + $currentState.MaximumRolloverFiles | Should -Be 0 + $currentState.ReserveDiskSpace | Should -BeNullOrEmpty + $currentState.OnFailure | Should -Be 'Continue' + $currentState.QueueDelay | Should -Be 1000 + $currentState.AuditGuid | Should -Be '06962963-ddd1-4a6b-86d6-0ef8d99b8e7b' + $currentState.Enabled | Should -BeTrue + } + } + } + } +} + +Describe 'SqlAudit\Modify()' -Tag 'Modify' { + Context 'When the system is not in the desired state' { + Context 'When audit is present but should be absent' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Ensure = 'Absent' + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru + } + + Mock -CommandName Remove-SqlDscAudit + } + + It 'Should call the correct mock' { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + Ensure = 'Absent' + Path = 'C:\Temp' + } + ) + + Should -Invoke -CommandName Remove-SqlDscAudit -Exactly -Times 1 -Scope It + } + } + } + + Context 'When audit is absent but should be present' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru + } + + Mock -CommandName New-SqlDscAudit -MockWith { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'), + 'MockAuditName' + ) + } -RemoveParameterValidation 'Path' + } + + It 'Should call the correct mock' { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + Ensure = 'Present' + Path = 'C:\Temp' + } + ) + + Should -Invoke -CommandName New-SqlDscAudit -Exactly -Times 1 -Scope It + } + } + + Context 'When the audit should also be enabled' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru + } + + Mock -CommandName Enable-SqlDscAudit + } + + It 'Should call the correct mocks' { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + Ensure = 'Present' + Path = 'C:\Temp' + Enabled = $true + } + ) + + Should -Invoke -CommandName New-SqlDscAudit -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Enable-SqlDscAudit -Exactly -Times 1 -Scope It + } + } + } + + Context 'When the audit should also be disabled' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru + } + + Mock -CommandName Disable-SqlDscAudit + } + + It 'Should call the correct mocks' { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + Ensure = 'Present' + Path = 'C:\Temp' + Enabled = $false + } + ) + + Should -Invoke -CommandName New-SqlDscAudit -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Disable-SqlDscAudit -Exactly -Times 1 -Scope It + } + } + } + + Context 'When the neither of the parameters LogType or Path was passed' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru + } + + Mock -CommandName Disable-SqlDscAudit + } + + It 'Should call the correct mocks' { + InModuleScope -ScriptBlock { + $mockErrorMessage = Get-InvalidOperationRecord -Message $mockSqlAuditInstance.localizedData.CannotCreateNewAudit + + { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + Ensure = 'Present' + Enabled = $false + } + ) + } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + } + } + + Context 'When audit should be enabled but is disabled' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Enabled = $false + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru + } + + Mock -CommandName Get-SqlDscAudit -MockWith { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'), + 'MockAuditName' + ) + } + + Mock -CommandName Enable-SqlDscAudit + } + + It 'Should call the correct mocks' { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + Enabled = $true + } + ) + + Should -Invoke -CommandName Get-SqlDscAudit -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Enable-SqlDscAudit -Exactly -Times 1 -Scope It + } + } + } + + Context 'When the audit should be disabled but is enabled' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Enabled = $true + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru + } + + Mock -CommandName Get-SqlDscAudit -MockWith { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'), + 'MockAuditName' + ) + } + + Mock -CommandName Disable-SqlDscAudit + } + + It 'Should call the correct mocks' { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + Enabled = $false + } + ) + + Should -Invoke -CommandName Get-SqlDscAudit -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Disable-SqlDscAudit -Exactly -Times 1 -Scope It + } + } + } + + Context 'When the property is not in desired state' -ForEach @( + @{ + MockPropertyName = 'Path' + MockExpectedValue = 'C:\NewValue' + } + @{ + MockPropertyName = 'AuditFilter' + MockExpectedValue = 'object -like ''something''' + } + @{ + MockPropertyName = 'MaximumFiles' + MockExpectedValue = 2 + } + @{ + MockPropertyName = 'MaximumRolloverFiles' + MockExpectedValue = 2 + } + @{ + MockPropertyName = 'OnFailure' + MockExpectedValue = 'FailOperation' + } + @{ + MockPropertyName = 'QueueDelay' + MockExpectedValue = 2000 + } + @{ + MockPropertyName = 'AuditGuid' + MockExpectedValue = 'cfa0d47e-bf93-41ab-bc9a-b8511acbcdd6' + } + ) { + BeforeAll { + InModuleScope -Parameters $_ -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + $MockPropertyName = $MockExpectedValue + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru + } + + Mock -CommandName Get-SqlDscAudit -MockWith { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'), + 'MockAuditName' + ) + } + + Mock -CommandName Set-SqlDscAudit -RemoveParameterValidation 'Path' + } + + It 'Should call the correct mocks' { + InModuleScope -Parameters $_ -ScriptBlock { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + $MockPropertyName = $MockExpectedValue + } + ) + + Should -Invoke -CommandName Get-SqlDscAudit -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Set-SqlDscAudit -ParameterFilter { + $PesterBoundParameters.$MockPropertyName -eq $MockExpectedValue + } -Exactly -Times 1 -Scope It + } + } + } + + Context 'When the property MaximumFileSize is not in desired state' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + MaximumFileSize = 20 + MaximumFileSizeUnit = 'Megabyte' + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru + } + + Mock -CommandName Get-SqlDscAudit -MockWith { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'), + 'MockAuditName' + ) + } + + Mock -CommandName Set-SqlDscAudit -RemoveParameterValidation 'Path' + } + + It 'Should call the correct mocks' { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + MaximumFileSize = 20 + } + ) + + Should -Invoke -CommandName Get-SqlDscAudit -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Set-SqlDscAudit -ParameterFilter { + $MaximumFileSize -eq 20 + } -Exactly -Times 1 -Scope It + } + } + } + + Context 'When the property MaximumFileSizeUnit is not in desired state' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + MaximumFileSize = 20 + MaximumFileSizeUnit = 'Megabyte' + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru + } + + Mock -CommandName Get-SqlDscAudit -MockWith { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'), + 'MockAuditName' + ) + } + + Mock -CommandName Set-SqlDscAudit -RemoveParameterValidation 'Path' + } + + It 'Should call the correct mocks' { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + MaximumFileSizeUnit = 'Megabyte' + } + ) + + Should -Invoke -CommandName Get-SqlDscAudit -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Set-SqlDscAudit -ParameterFilter { + $MaximumFileSizeUnit -eq 'Megabyte' + } -Exactly -Times 1 -Scope It + } + } + } + + Context 'When the property ReservDiskSpace is not in desired state' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + MaximumFiles = 20 + ReserveDiskSpace = $true + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru + } + + Mock -CommandName Get-SqlDscAudit -MockWith { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'), + 'MockAuditName' + ) + } + + Mock -CommandName Set-SqlDscAudit -RemoveParameterValidation 'Path' + } + + It 'Should call the correct mocks' { + InModuleScope -Parameters $_ -ScriptBlock { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + MaximumFileSizeUnit = 'Megabyte' + } + ) + + Should -Invoke -CommandName Get-SqlDscAudit -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Set-SqlDscAudit -ParameterFilter { + $ReserveDiskSpace -eq $true + } -Exactly -Times 1 -Scope It + } + } + } + + Context 'When trying to change a File audit property when audit type is of a Log-type' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + MaximumFiles = 20 + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru + } + + Mock -CommandName Get-SqlDscAudit -MockWith { + $mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'), + 'MockAuditName' + ) + + $mockAuditObject.DestinationType = 'SecurityLog' + + return $mockAuditObject + } + + Mock -CommandName Set-SqlDscAudit -RemoveParameterValidation 'Path' + } + + It 'Should call the correct mocks' { + InModuleScope -Parameters $_ -ScriptBlock { + $mockErrorMessage = Get-InvalidOperationRecord -Message ( + $mockSqlAuditInstance.localizedData.AuditOfWrongTypeForUseWithProperty -f 'SecurityLog' + ) + + { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + MaximumFiles = 20 + } + ) + } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + } + + Context 'When trying to change Path but audit type is of a Log-type' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + Force = $true + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'CreateAudit' -Value { + $script:mockMethodCreateAuditCallCount += 1 + } -PassThru + } + + Mock -CommandName Get-SqlDscAudit -MockWith { + $mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'), + 'MockAuditName' + ) + + $mockAuditObject.DestinationType = 'SecurityLog' + + return $mockAuditObject + } + + Mock -CommandName Remove-SqlDscAudit + } + + BeforeEach { + InModuleScope -ScriptBlock { + $script:mockMethodCreateAuditCallCount = 0 + } + } + + It 'Should call the correct mocks' { + InModuleScope -Parameters $_ -ScriptBlock { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + Path = 'C:\Temp' + } + ) + + Should -Invoke -CommandName Remove-SqlDscAudit -Exactly -Times 1 -Scope It + + $script:mockMethodCreateAuditCallCount | Should -Be 1 + } + } + } + + Context 'When trying to change LogType but audit type is a File-type' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + LogType = 'ApplicationLog' + Force = $true + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'CreateAudit' -Value { + $script:mockMethodCreateAuditCallCount += 1 + } -PassThru + } + + Mock -CommandName Get-SqlDscAudit -MockWith { + $mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'), + 'MockAuditName' + ) + + $mockAuditObject.DestinationType = 'File' + + return $mockAuditObject + } + + Mock -CommandName Remove-SqlDscAudit + } + + BeforeEach { + InModuleScope -ScriptBlock { + $script:mockMethodCreateAuditCallCount = 0 + } + } + + It 'Should call the correct mocks' { + InModuleScope -Parameters $_ -ScriptBlock { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + LogType = 'ApplicationLog' + } + ) + + Should -Invoke -CommandName Remove-SqlDscAudit -Exactly -Times 1 -Scope It + + $script:mockMethodCreateAuditCallCount | Should -Be 1 + } + } + } + + Context 'When trying to change Path but audit type is of a Log-type and Force is not set to $true' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'CreateAudit' -Value { + $script:mockMethodCreateAuditCallCount += 1 + } -PassThru + } + + Mock -CommandName Get-SqlDscAudit -MockWith { + $mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'), + 'MockAuditName' + ) + + $mockAuditObject.DestinationType = 'SecurityLog' + + return $mockAuditObject + } + + Mock -CommandName Remove-SqlDscAudit + } + + BeforeEach { + InModuleScope -ScriptBlock { + $script:mockMethodCreateAuditCallCount = 0 + } + } + + It 'Should throw the correct error' { + InModuleScope -Parameters $_ -ScriptBlock { + $mockErrorMessage = Get-InvalidOperationRecord -Message ( + $mockSqlAuditInstance.localizedData.AuditIsWrongType + ) + + { + $script:mockSqlAuditInstance.Modify( + # This is the properties not in desired state. + @{ + Path = 'C:\Temp' + } + ) + } | Should -Throw -ExpectedMessage $mockErrorMessage + + Should -Invoke -CommandName Remove-SqlDscAudit -Exactly -Times 0 -Scope It + + $script:mockMethodCreateAuditCallCount | Should -Be 0 + } + } + } + } +} + +Describe 'SqlAudit\AssertProperties()' -Tag 'AssertProperties' { + Context 'When the path does not exist' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } + } + + Mock -CommandName Test-Path -MockWith { + return $false + } + } + + It 'Should throw the correct error for Get()' { + InModuleScope -ScriptBlock { + $mockErrorMessage = $script:mockSqlAuditInstance.localizedData.PathInvalid -f 'C:\Temp' + + $mockErrorMessage += ' (Parameter ''Path'')' + + { $script:mockSqlAuditInstance.Get() } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + + It 'Should throw the correct error for Set()' { + InModuleScope -ScriptBlock { + $mockErrorMessage = $script:mockSqlAuditInstance.localizedData.PathInvalid -f 'C:\Temp' + + $mockErrorMessage += ' (Parameter ''Path'')' + + { $script:mockSqlAuditInstance.Set() } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + + It 'Should throw the correct error for Test()' { + InModuleScope -ScriptBlock { + $mockErrorMessage = $script:mockSqlAuditInstance.localizedData.PathInvalid -f 'C:\Temp' + + $mockErrorMessage += ' (Parameter ''Path'')' + + { $script:mockSqlAuditInstance.Test() } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + } + + <# + These tests just check for the string localized ID. Since the error is part + of a command outside of SqlServerDsc, a small changes to the localized + string should not fail these tests. + #> + Context 'When passing mutually exclusive parameters' { + Context 'When passing MaximumFiles and MaximumRolloverFiles' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } + } + } + + It 'Should throw the correct error' { + InModuleScope -ScriptBlock { + { + $mockSqlAuditInstance.AssertProperties( + @{ + MaximumFiles = 2 + MaximumRolloverFiles = 2 + } + ) + } | Should -Throw -ExpectedMessage '*DRC0010*' + } + } + } + + Context 'When passing LogType and a File audit property' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + } + } + } + + It 'Should throw the correct error for property ''''' -ForEach @( + @{ + MockPropertyName = 'Path' + } + @{ + MockPropertyName = 'MaximumFiles' + } + @{ + MockPropertyName = 'MaximumFileSize' + } + @{ + MockPropertyName = 'MaximumFileSizeUnit' + } + @{ + MockPropertyName = 'MaximumRolloverFiles' + } + @{ + MockPropertyName = 'ReserveDiskSpace' + } + ) { + InModuleScope -Parameters $_ -ScriptBlock { + { + $mockSqlAuditInstance.AssertProperties( + @{ + LogType = 'SecurityLog' + $MockPropertyName = 'AnyValue' + } + ) + } | Should -Throw -ExpectedMessage '*DRC0010*' + } + } + } + + Context 'When passing just one of either MaximumFileSize and MaximumFileSizeUnit' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } + } + } + + It 'Should throw the correct error for property ''''' -ForEach @( + @{ + MockPropertyName = 'MaximumFileSize' + } + @{ + MockPropertyName = 'MaximumFileSizeUnit' + } + ) { + InModuleScope -Parameters $_ -ScriptBlock { + $mockErrorMessage = $script:mockSqlAuditInstance.localizedData.BothFileSizePropertiesMustBeSet + + $mockErrorMessage += ' (Parameter ''MaximumFileSize, MaximumFileSizeUnit'')' + + { + $mockSqlAuditInstance.AssertProperties( + @{ + $MockPropertyName = 'AnyValue' + } + ) + } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + } + + Context 'When passing MaximumFileSize with a value of 1' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } + } + } + + It 'Should throw the correct error' { + InModuleScope -ScriptBlock { + $mockErrorMessage = $script:mockSqlAuditInstance.localizedData.MaximumFileSizeValueInvalid + + $mockErrorMessage += ' (Parameter ''MaximumFileSize'')' + + { + $mockSqlAuditInstance.AssertProperties( + @{ + MaximumFileSize = 1 + MaximumFileSizeUnit = 'Megabyte' + } + ) + } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + } + + Context 'When passing QueueDelay with an invalid value' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } + } + } + + It 'Should throw the correct error with value ' -ForEach @( + @{ + MockQueueDelayValue = 1 + } + @{ + MockQueueDelayValue = 457 + } + @{ + MockQueueDelayValue = 800 + } + @{ + MockQueueDelayValue = 999 + } + ) { + InModuleScope -Parameters $_ -ScriptBlock { + $mockErrorMessage = $script:mockSqlAuditInstance.localizedData.QueueDelayValueInvalid + + $mockErrorMessage += ' (Parameter ''QueueDelay'')' + + { + $mockSqlAuditInstance.AssertProperties( + @{ + QueueDelay = $MockQueueDelayValue + } + ) + } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + } + + Context 'When passing ReserveDiskSpace without passing MaximumFiles' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAuditInstance = [SqlAudit] @{ + Name = 'MockAuditName' + InstanceName = 'NamedInstance' + Path = 'C:\Temp' + } + } + } + + It 'Should throw the correct error' { + InModuleScope -ScriptBlock { + $mockErrorMessage = $script:mockSqlAuditInstance.localizedData.BothFileSizePropertiesMustBeSet + + $mockErrorMessage += ' (Parameter ''ReserveDiskSpace'')' + + { + $mockSqlAuditInstance.AssertProperties( + @{ + ReserveDiskSpace = $true + } + ) + } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + } + } +} diff --git a/tests/Unit/Classes/SqlResourceBase.Tests.ps1 b/tests/Unit/Classes/SqlResourceBase.Tests.ps1 new file mode 100644 index 000000000..d46646ffd --- /dev/null +++ b/tests/Unit/Classes/SqlResourceBase.Tests.ps1 @@ -0,0 +1,134 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'SqlResourceBase' { + Context 'When class is instantiated' { + It 'Should not throw an exception' { + InModuleScope -ScriptBlock { + { [SqlResourceBase]::new() } | Should -Not -Throw + } + } + + It 'Should have a default constructor' { + InModuleScope -ScriptBlock { + $instance = [SqlResourceBase]::new() + $instance | Should -Not -BeNullOrEmpty + $instance.SqlServerObject | Should -BeNullOrEmpty + } + } + + It 'Should be the correct type' { + InModuleScope -ScriptBlock { + $instance = [SqlResourceBase]::new() + $instance.GetType().Name | Should -Be 'SqlResourceBase' + } + } + } +} + +Describe 'SqlResourceBase\GetServerObject()' -Tag 'GetServerObject' { + Context 'When a server object does not exist' { + BeforeAll { + $mockSqlResourceBaseInstance = InModuleScope -ScriptBlock { + [SqlResourceBase]::new() + } + + Mock -CommandName Connect-SqlDscDatabaseEngine -MockWith { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } + } + + It 'Should call the correct mock' { + $result = $mockSqlResourceBaseInstance.GetServerObject() + + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Server' + + Should -Invoke -CommandName Connect-SqlDscDatabaseEngine -Exactly -Times 1 -Scope It + } + + Context 'When property Credential is used' { + BeforeAll { + $mockSqlResourceBaseInstance = InModuleScope -ScriptBlock { + [SqlResourceBase]::new() + } + + $mockSqlResourceBaseInstance.Credential = [System.Management.Automation.PSCredential]::new( + 'MyCredentialUserName', + [SecureString]::new() + ) + } + + It 'Should call the correct mock' { + $result = $mockSqlResourceBaseInstance.GetServerObject() + + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Server' + + Should -Invoke -CommandName Connect-SqlDscDatabaseEngine -ParameterFilter { + $PesterBoundParameters.Keys -contains 'Credential' + } -Exactly -Times 1 -Scope It + } + } + } + + Context 'When a server object already exist' { + BeforeAll { + $mockSqlResourceBaseInstance = InModuleScope -ScriptBlock { + [SqlResourceBase]::new() + } + + $mockSqlResourceBaseInstance.SqlServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + Mock -CommandName Connect-SqlDscDatabaseEngine + } + + It 'Should call the correct mock' { + $result = $mockSqlResourceBaseInstance.GetServerObject() + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Server' + + Should -Invoke -CommandName Connect-SqlDscDatabaseEngine -Exactly -Times 0 -Scope It + } + } +} diff --git a/tests/Unit/Private/ConvertTo-Reason.Tests.ps1 b/tests/Unit/Private/ConvertTo-Reason.Tests.ps1 index 7926d5451..e2badb213 100644 --- a/tests/Unit/Private/ConvertTo-Reason.Tests.ps1 +++ b/tests/Unit/Private/ConvertTo-Reason.Tests.ps1 @@ -124,4 +124,90 @@ Describe 'ConvertTo-Reason' -Tag 'Private' { } } } + + Context 'When ExpectedValue has $null for a property' { + Context 'When on Windows PowerShell' { + BeforeAll { + $script:originalPSEdition = $PSVersionTable.PSEdition + + $PSVersionTable.PSEdition = 'Desktop' + } + + AfterAll { + $PSVersionTable.PSEdition = $script:originalPSEdition + } + It 'Should return the correct values in a hashtable' { + InModuleScope -ScriptBlock { + $mockProperties = @( + @{ + Property = 'MyResourceProperty1' + ExpectedValue = $null + ActualValue = 'MyValue1' + } + ) + + $result = ConvertTo-Reason -Property $mockProperties -ResourceName 'MyResource' + + $result | Should -HaveCount 1 + + $result.Code | Should -Contain 'MyResource:MyResource:MyResourceProperty1' + $result.Phrase | Should -Contain 'The property MyResourceProperty1 should be "", but was "MyValue1"' + } + } + } + } + + Context 'When ActualValue has $null for a property' { + Context 'When on Windows PowerShell' { + BeforeAll { + $script:originalPSEdition = $PSVersionTable.PSEdition + + $PSVersionTable.PSEdition = 'Desktop' + } + + AfterAll { + $PSVersionTable.PSEdition = $script:originalPSEdition + } + + It 'Should return the correct values in a hashtable' { + InModuleScope -ScriptBlock { + $mockProperties = @( + @{ + Property = 'MyResourceProperty1' + ExpectedValue = 'MyValue1' + ActualValue = $null + } + ) + + $result = ConvertTo-Reason -Property $mockProperties -ResourceName 'MyResource' + + $result | Should -HaveCount 1 + + $result.Code | Should -Contain 'MyResource:MyResource:MyResourceProperty1' + $result.Phrase | Should -Contain 'The property MyResourceProperty1 should be "MyValue1", but was ""' + } + } + } + } + + Context 'When a path property contain double backslash' { + It 'Should return the correct values in a hashtable' { + InModuleScope -ScriptBlock { + $mockProperties = @( + @{ + Property = 'MyResourcePathProperty' + ExpectedValue = 'C:\Temp\MyFolder' + ActualValue = 'C:\Temp\MyNewFolder' + } + ) + + $result = ConvertTo-Reason -Property $mockProperties -ResourceName 'MyResource' + + $result | Should -HaveCount 1 + + $result.Code | Should -Contain 'MyResource:MyResource:MyResourcePathProperty' + $result.Phrase | Should -Contain 'The property MyResourcePathProperty should be "C:\Temp\MyFolder", but was "C:\Temp\MyNewFolder"' + } + } + } } diff --git a/tests/Unit/Private/Get-DscProperty.Tests.ps1 b/tests/Unit/Private/Get-DscProperty.Tests.ps1 index f4bd4539e..9ac1295ae 100644 --- a/tests/Unit/Private/Get-DscProperty.Tests.ps1 +++ b/tests/Unit/Private/Get-DscProperty.Tests.ps1 @@ -752,4 +752,66 @@ $script:mockResourceBaseInstance.MyResourceProperty2 = 'MockValue5' } } } + + Context 'When excluding specific property names' { + BeforeAll { + <# + Must use a here-string because we need to pass 'using' which must be + first in a scriptblock, but if it is outside the here-string then + PowerShell will fail to parse the test script. + #> + $inModuleScopeScriptBlock = @' +class MyMockResource +{ +[DscProperty(Key)] +[System.String] +$MyResourceKeyProperty1 + +[DscProperty(Key)] +[System.String] +$MyResourceKeyProperty2 + +[DscProperty(Mandatory)] +[System.String] +$MyResourceMandatoryProperty + +[DscProperty()] +[System.String] +$MyResourceProperty1 + +[DscProperty()] +[System.String] +$MyResourceProperty2 + +[DscProperty(NotConfigurable)] +[System.String] +$MyResourceReadProperty +} + +$script:mockResourceBaseInstance = [MyMockResource]::new() +$script:mockResourceBaseInstance.MyResourceKeyProperty1 = 'MockValue1' +$script:mockResourceBaseInstance.MyResourceKeyProperty2 = 'MockValue2' +$script:mockResourceBaseInstance.MyResourceMandatoryProperty = 'MockValue3' +$script:mockResourceBaseInstance.MyResourceProperty1 = 'MockValue5' +$script:mockResourceBaseInstance.MyResourceProperty2 = 'MockValue6' +'@ + + InModuleScope -ScriptBlock ([Scriptblock]::Create($inModuleScopeScriptBlock)) + } + + It 'Should return the correct value' { + InModuleScope -ScriptBlock { + $result = Get-DscProperty -ExcludeName @('MyResourceKeyProperty1', 'MyResourceProperty1') -HasValue -InputObject $script:mockResourceBaseInstance + + $result | Should -BeOfType [System.Collections.Hashtable] + + $result.Keys | Should -Not -Contain 'MyResourceKeyProperty1' -Because 'the property was excluded' + $result.Keys | Should -Not -Contain 'MyResourceProperty1' -Because 'the property was excluded' + + $result.Keys | Should -Contain 'MyResourceKeyProperty2' -Because 'the property has a non-null value and was not excluded' + $result.Keys | Should -Contain 'MyResourceProperty2' -Because 'the property has a non-null value and was not excluded' + $result.Keys | Should -Contain 'MyResourceMandatoryProperty' -Because 'the property has a non-null value and was not excluded' + } + } + } } diff --git a/tests/Unit/Public/Disable-SqlDscAudit.Tests.ps1 b/tests/Unit/Public/Disable-SqlDscAudit.Tests.ps1 new file mode 100644 index 000000000..fa171c110 --- /dev/null +++ b/tests/Unit/Public/Disable-SqlDscAudit.Tests.ps1 @@ -0,0 +1,190 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Disable-SqlDscAudit' -Tag 'Public' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + MockParameterSetName = 'ServerObject' + MockExpectedParameters = '-ServerObject -Name [-Force] [-Refresh] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'AuditObject' + MockExpectedParameters = '-AuditObject [-Force] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Disable-SqlDscAudit').ParameterSets | + Where-Object -FilterScript { + $_.Name -eq $mockParameterSetName + } | + Select-Object -Property @( + @{ + Name = 'ParameterSetName' + Expression = { $_.Name } + }, + @{ + Name = 'ParameterListAsString' + Expression = { $_.ToString() } + } + ) + + $result.ParameterSetName | Should -Be $MockParameterSetName + $result.ParameterListAsString | Should -Be $MockExpectedParameters + } + + Context 'When enabling an audit by ServerObject' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + Mock -CommandName Get-SqlDscAudit -MockWith { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Disable' -Value { + $script:mockMethodDisableCallCount += 1 + } -PassThru -Force + } + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + } + } + + BeforeEach { + $script:mockMethodDisableCallCount = 0 + } + + Context 'When using parameter Confirm with value $false' { + It 'Should call the mocked method and have correct values in the object' { + Disable-SqlDscAudit -Confirm:$false @mockDefaultParameters + + $mockMethodDisableCallCount | Should -Be 1 + } + } + + Context 'When using parameter Force' { + It 'Should call the mocked method and have correct values in the object' { + Disable-SqlDscAudit -Force @mockDefaultParameters + + $mockMethodDisableCallCount | Should -Be 1 + } + } + + Context 'When using parameter WhatIf' { + It 'Should call the mocked method and have correct values in the object' { + Disable-SqlDscAudit -WhatIf @mockDefaultParameters + + $mockMethodDisableCallCount | Should -Be 0 + } + } + + Context 'When passing parameter ServerObject over the pipeline' { + It 'Should call the mocked method and have correct values in the object' { + $mockServerObject | Disable-SqlDscAudit -Name 'Log1' -Force + + $mockMethodDisableCallCount | Should -Be 1 + } + } + } + + Context 'When removing an audit by AuditObject' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Disable' -Value { + $script:mockMethodDisableCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + } + } + + BeforeEach { + $script:mockMethodDisableCallCount = 0 + } + + Context 'When using parameter Confirm with value $false' { + It 'Should call the mocked method and have correct values in the object' { + Disable-SqlDscAudit -Confirm:$false @mockDefaultParameters + + $mockMethodDisableCallCount | Should -Be 1 + } + } + + Context 'When using parameter Force' { + It 'Should call the mocked method and have correct values in the object' { + Disable-SqlDscAudit -Force @mockDefaultParameters + + $mockMethodDisableCallCount | Should -Be 1 + } + } + + Context 'When using parameter WhatIf' { + It 'Should call the mocked method and have correct values in the object' { + Disable-SqlDscAudit -WhatIf @mockDefaultParameters + + $mockMethodDisableCallCount | Should -Be 0 + } + } + + Context 'When passing parameter ServerObject over the pipeline' { + It 'Should call the mocked method and have correct values in the object' { + $mockAuditObject | Disable-SqlDscAudit -Force + + $mockMethodDisableCallCount | Should -Be 1 + } + } + } +} diff --git a/tests/Unit/Public/Enable-SqlDscAudit.Tests.ps1 b/tests/Unit/Public/Enable-SqlDscAudit.Tests.ps1 new file mode 100644 index 000000000..026cbe25d --- /dev/null +++ b/tests/Unit/Public/Enable-SqlDscAudit.Tests.ps1 @@ -0,0 +1,190 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Enable-SqlDscAudit' -Tag 'Public' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + MockParameterSetName = 'ServerObject' + MockExpectedParameters = '-ServerObject -Name [-Force] [-Refresh] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'AuditObject' + MockExpectedParameters = '-AuditObject [-Force] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Enable-SqlDscAudit').ParameterSets | + Where-Object -FilterScript { + $_.Name -eq $mockParameterSetName + } | + Select-Object -Property @( + @{ + Name = 'ParameterSetName' + Expression = { $_.Name } + }, + @{ + Name = 'ParameterListAsString' + Expression = { $_.ToString() } + } + ) + + $result.ParameterSetName | Should -Be $MockParameterSetName + $result.ParameterListAsString | Should -Be $MockExpectedParameters + } + + Context 'When enabling an audit by ServerObject' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + Mock -CommandName Get-SqlDscAudit -MockWith { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Enable' -Value { + $script:mockMethodEnableCallCount += 1 + } -PassThru -Force + } + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + } + } + + BeforeEach { + $script:mockMethodEnableCallCount = 0 + } + + Context 'When using parameter Confirm with value $false' { + It 'Should call the mocked method and have correct values in the object' { + Enable-SqlDscAudit -Confirm:$false @mockDefaultParameters + + $mockMethodEnableCallCount | Should -Be 1 + } + } + + Context 'When using parameter Force' { + It 'Should call the mocked method and have correct values in the object' { + Enable-SqlDscAudit -Force @mockDefaultParameters + + $mockMethodEnableCallCount | Should -Be 1 + } + } + + Context 'When using parameter WhatIf' { + It 'Should call the mocked method and have correct values in the object' { + Enable-SqlDscAudit -WhatIf @mockDefaultParameters + + $mockMethodEnableCallCount | Should -Be 0 + } + } + + Context 'When passing parameter ServerObject over the pipeline' { + It 'Should call the mocked method and have correct values in the object' { + $mockServerObject | Enable-SqlDscAudit -Name 'Log1' -Force + + $mockMethodEnableCallCount | Should -Be 1 + } + } + } + + Context 'When removing an audit by AuditObject' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Enable' -Value { + $script:mockMethodEnableCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + } + } + + BeforeEach { + $script:mockMethodEnableCallCount = 0 + } + + Context 'When using parameter Confirm with value $false' { + It 'Should call the mocked method and have correct values in the object' { + Enable-SqlDscAudit -Confirm:$false @mockDefaultParameters + + $mockMethodEnableCallCount | Should -Be 1 + } + } + + Context 'When using parameter Force' { + It 'Should call the mocked method and have correct values in the object' { + Enable-SqlDscAudit -Force @mockDefaultParameters + + $mockMethodEnableCallCount | Should -Be 1 + } + } + + Context 'When using parameter WhatIf' { + It 'Should call the mocked method and have correct values in the object' { + Enable-SqlDscAudit -WhatIf @mockDefaultParameters + + $mockMethodEnableCallCount | Should -Be 0 + } + } + + Context 'When passing parameter ServerObject over the pipeline' { + It 'Should call the mocked method and have correct values in the object' { + $mockAuditObject | Enable-SqlDscAudit -Force + + $mockMethodEnableCallCount | Should -Be 1 + } + } + } +} diff --git a/tests/Unit/Public/Get-SqlDscAudit.Tests.ps1 b/tests/Unit/Public/Get-SqlDscAudit.Tests.ps1 new file mode 100644 index 000000000..fe1b3bb44 --- /dev/null +++ b/tests/Unit/Public/Get-SqlDscAudit.Tests.ps1 @@ -0,0 +1,120 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Get-SqlDscAudit' -Tag 'Public' { + Context 'When no audit exist' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' | + Add-Member -MemberType 'ScriptProperty' -Name 'Audits' -Value { + return @{} + } -PassThru -Force + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + } + } + + Context 'When specifying to throw on error' { + BeforeAll { + $mockErrorMessage = InModuleScope -ScriptBlock { + $script:localizedData.Audit_Missing + } + } + + It 'Should throw the correct error' { + { Get-SqlDscAudit @mockDefaultParameters -ErrorAction 'Stop' } | + Should -Throw -ExpectedMessage ($mockErrorMessage -f 'Log1') + } + } + + Context 'When ignoring the error' { + It 'Should not throw an exception and return $null' { + Get-SqlDscAudit @mockDefaultParameters -ErrorAction 'SilentlyContinue' | + Should -BeNullOrEmpty + } + } + } + + Context 'When getting a specific audit' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockServerObject = $mockServerObject | + Add-Member -MemberType 'ScriptProperty' -Name 'Audits' -Value { + return @{ + 'Log1' = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) + } + } -PassThru -Force + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + } + } + + It 'Should return the correct values' { + $result = Get-SqlDscAudit @mockDefaultParameters + + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Audit' + $result.Name | Should -Be 'Log1' + } + + Context 'When passing parameter ServerObject over the pipeline' { + It 'Should return the correct values' { + $result = Get-SqlDscAudit @mockDefaultParameters + + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Audit' + $result.Name | Should -Be 'Log1' + } + } + } +} diff --git a/tests/Unit/Public/Invoke-SqlDscQuery.Tests.ps1 b/tests/Unit/Public/Invoke-SqlDscQuery.Tests.ps1 new file mode 100644 index 000000000..9533cca2e --- /dev/null +++ b/tests/Unit/Public/Invoke-SqlDscQuery.Tests.ps1 @@ -0,0 +1,146 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Invoke-SqlDscQuery' -Tag 'Public' { + Context 'When calling the command with only mandatory parameters' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + + Mock -CommandName Invoke-Query + } + + It 'Should execute the query without throwing and without returning any result' { + $result = Invoke-SqlDscQuery -ServerObject $mockServerObject -DatabaseName 'master' -Query 'select name from sys.databases' + + $result | Should -BeNullOrEmpty + + Should -Invoke -CommandName Invoke-Query -ParameterFilter { + $PesterBoundParameters.Keys -contains 'SqlServerObject' -and + $PesterBoundParameters.Keys -contains 'Database' -and + $PesterBoundParameters.Keys -contains 'Query' + } -Exactly -Times 1 -Scope It + } + + Context 'When passing ServerObject over the pipeline' { + It 'Should execute the query without throwing and without returning any result' { + $result = $mockServerObject | Invoke-SqlDscQuery -DatabaseName 'master' -Query 'select name from sys.databases' + + $result | Should -BeNullOrEmpty + + Should -Invoke -CommandName Invoke-Query -ParameterFilter { + $PesterBoundParameters.Keys -contains 'SqlServerObject' -and + $PesterBoundParameters.Keys -contains 'Database' -and + $PesterBoundParameters.Keys -contains 'Query' + } -Exactly -Times 1 -Scope It + } + } + } + + Context 'When calling the command with optional parameter StatementTimeout' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + + Mock -CommandName Invoke-Query + } + + It 'Should execute the query without throwing and without returning any result' { + $result = Invoke-SqlDscQuery -StatementTimeout 900 -ServerObject $mockServerObject -DatabaseName 'master' -Query 'select name from sys.databases' + + $result | Should -BeNullOrEmpty + + Should -Invoke -CommandName Invoke-Query -ParameterFilter { + $PesterBoundParameters.Keys -contains 'SqlServerObject' -and + $PesterBoundParameters.Keys -contains 'Database' -and + $PesterBoundParameters.Keys -contains 'Query' -and + $PesterBoundParameters.Keys -contains 'StatementTimeout' + } -Exactly -Times 1 -Scope It + } + } + + Context 'When calling the command with optional parameter RedactText' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + + Mock -CommandName Invoke-Query + } + + It 'Should execute the query without throwing and without returning any result' { + $result = Invoke-SqlDscQuery -RedactText @('MyString') -ServerObject $mockServerObject -DatabaseName 'master' -Query 'select name from sys.databases' + + $result | Should -BeNullOrEmpty + + Should -Invoke -CommandName Invoke-Query -ParameterFilter { + $PesterBoundParameters.Keys -contains 'SqlServerObject' -and + $PesterBoundParameters.Keys -contains 'Database' -and + $PesterBoundParameters.Keys -contains 'Query' -and + $PesterBoundParameters.Keys -contains 'RedactText' + } -Exactly -Times 1 -Scope It + } + } + + Context 'When calling the command with optional parameter PassThru' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + + # Actual testing that Invoke-Query returns values is done in the unit tests for Invoke-Query. + Mock -CommandName Invoke-Query + } + + It 'Should execute the query without throwing and without returning any result' { + $result = Invoke-SqlDscQuery -PassThru -ServerObject $mockServerObject -DatabaseName 'master' -Query 'select name from sys.databases' + + $result | Should -BeNullOrEmpty + + Should -Invoke -CommandName Invoke-Query -ParameterFilter { + $PesterBoundParameters.Keys -contains 'SqlServerObject' -and + $PesterBoundParameters.Keys -contains 'Database' -and + $PesterBoundParameters.Keys -contains 'Query' -and + $PesterBoundParameters.Keys -contains 'WithResults' + } -Exactly -Times 1 -Scope It + } + } +} diff --git a/tests/Unit/Public/New-SqlDscAudit.Tests.ps1 b/tests/Unit/Public/New-SqlDscAudit.Tests.ps1 new file mode 100644 index 000000000..05447edff --- /dev/null +++ b/tests/Unit/Public/New-SqlDscAudit.Tests.ps1 @@ -0,0 +1,913 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'New-SqlDscAudit' -Tag 'Public' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + MockParameterSetName = 'Log' + MockExpectedParameters = '-ServerObject -Name -LogType [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'File' + MockExpectedParameters = '-ServerObject -Name -Path [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'FileWithSize' + MockExpectedParameters = '-ServerObject -Name -Path -MaximumFileSize -MaximumFileSizeUnit [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'FileWithMaxFiles' + MockExpectedParameters = '-ServerObject -Name -Path -MaximumFiles [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-ReserveDiskSpace] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'FileWithMaxRolloverFiles' + MockExpectedParameters = '-ServerObject -Name -Path -MaximumRolloverFiles [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'FileWithSizeAndMaxFiles' + MockExpectedParameters = '-ServerObject -Name -Path -MaximumFileSize -MaximumFileSizeUnit -MaximumFiles [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-ReserveDiskSpace] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'FileWithSizeAndMaxRolloverFiles' + MockExpectedParameters = '-ServerObject -Name -Path -MaximumFileSize -MaximumFileSizeUnit -MaximumRolloverFiles [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'New-SqlDscAudit').ParameterSets | + Where-Object -FilterScript { + $_.Name -eq $mockParameterSetName + } | + Select-Object -Property @( + @{ + Name = 'ParameterSetName' + Expression = { $_.Name } + }, + @{ + Name = 'ParameterListAsString' + Expression = { $_.ToString() } + } + ) + + $result.ParameterSetName | Should -Be $MockParameterSetName + $result.ParameterListAsString | Should -Be $MockExpectedParameters + } + + Context 'When adding an application log audit using mandatory parameters' { + BeforeAll { + $script:mockCreateAuditObject = $null + + Mock -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Audit' + } -MockWith { + <# + The Audit object is created in the script scope so that the + properties can be validated. + #> + $script:mockCreateAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $PesterBoundParameters.ArgumentList[0], + $PesterBoundParameters.ArgumentList[1] + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { + $script:mockMethodCreateCallCount += 1 + } -PassThru -Force + + return $script:mockCreateAuditObject + } + + Mock -CommandName Get-SqlDscAudit + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + LogType = 'ApplicationLog' + Name = 'Log1' + } + } + + BeforeEach { + $script:mockMethodCreateCallCount = 0 + } + + Context 'When using parameter Confirm with value $false' { + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -Confirm:$false @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'ApplicationLog' + + $mockMethodCreateCallCount | Should -Be 1 + } + } + + Context 'When using parameter Force' { + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -Force @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'ApplicationLog' + + $mockMethodCreateCallCount | Should -Be 1 + } + } + + Context 'When using parameter WhatIf' { + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -WhatIf @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'ApplicationLog' + + $mockMethodCreateCallCount | Should -Be 0 + } + } + + Context 'When passing parameter ServerObject over the pipeline' { + It 'Should call the mocked method and have correct values in the object' { + $mockServerObject | New-SqlDscAudit -LogType 'ApplicationLog' -Name 'Log1' -Force + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'ApplicationLog' + + $mockMethodCreateCallCount | Should -Be 1 + } + } + } + + Context 'When adding an security log audit using mandatory parameters' { + BeforeAll { + $script:mockCreateAuditObject = $null + + Mock -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Audit' + } -MockWith { + <# + The Audit object is created in the script scope so that the + properties can be validated. + #> + $script:mockCreateAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $PesterBoundParameters.ArgumentList[0], + $PesterBoundParameters.ArgumentList[1] + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { + $script:mockMethodCreateCallCount += 1 + } -PassThru -Force + + return $script:mockCreateAuditObject + } + + Mock -CommandName Get-SqlDscAudit + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + LogType = 'SecurityLog' + Name = 'Log1' + } + } + + BeforeEach { + $script:mockMethodCreateCallCount = 0 + } + + Context 'When using parameter Confirm with value $false' { + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -Confirm:$false @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'SecurityLog' + + $mockMethodCreateCallCount | Should -Be 1 + } + } + + Context 'When using parameter Force' { + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -Force @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'SecurityLog' + + $mockMethodCreateCallCount | Should -Be 1 + } + } + + Context 'When using parameter WhatIf' { + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -WhatIf @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'SecurityLog' + + $mockMethodCreateCallCount | Should -Be 0 + } + } + + Context 'When passing parameter ServerObject over the pipeline' { + It 'Should call the mocked method and have correct values in the object' { + $mockServerObject | New-SqlDscAudit -LogType 'SecurityLog' -Name 'Log1' -Force + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'SecurityLog' + + $mockMethodCreateCallCount | Should -Be 1 + } + } + } + + Context 'When adding an file audit using mandatory parameters' { + BeforeAll { + $script:mockCreateAuditObject = $null + + Mock -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Audit' + } -MockWith { + <# + The Audit object is created in the script scope so that the + properties can be validated. + #> + $script:mockCreateAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $PesterBoundParameters.ArgumentList[0], + $PesterBoundParameters.ArgumentList[1] + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { + $script:mockMethodCreateCallCount += 1 + } -PassThru -Force + + return $script:mockCreateAuditObject + } + + Mock -CommandName Get-SqlDscAudit + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + Path = Get-TemporaryFolder + } + } + + BeforeEach { + $script:mockMethodCreateCallCount = 0 + } + + Context 'When using parameter Confirm with value $false' { + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -Confirm:$false @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'File' + $mockCreateAuditObject.FilePath | Should -Be (Get-TemporaryFolder) + + $mockMethodCreateCallCount | Should -Be 1 + } + } + + Context 'When using parameter Force' { + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -Force @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'File' + $mockCreateAuditObject.FilePath | Should -Be (Get-TemporaryFolder) + + $mockMethodCreateCallCount | Should -Be 1 + } + } + + Context 'When using parameter WhatIf' { + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -WhatIf @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'File' + $mockCreateAuditObject.FilePath | Should -Be (Get-TemporaryFolder) + + $mockMethodCreateCallCount | Should -Be 0 + } + } + + Context 'When passing parameter ServerObject over the pipeline' { + It 'Should call the mocked method and have correct values in the object' { + $mockServerObject | New-SqlDscAudit -Path (Get-TemporaryFolder) -Name 'Log1' -Force + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'File' + $mockCreateAuditObject.FilePath | Should -Be (Get-TemporaryFolder) + + $mockMethodCreateCallCount | Should -Be 1 + } + } + } + + Context 'When adding an file audit and passing an invalid path' { + BeforeAll { + Mock -CommandName Test-Path -MockWith { + return $false + } + } + + It 'Should throw the correct error' { + $mockNewSqlDscAuditParameters = @{ + ServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + Path = Get-TemporaryFolder + Name = 'Log1' + } + + $mockErrorMessage = InModuleScope -ScriptBlock { + $script:localizedData.Audit_PathParameterValueInvalid + } + + $mockErrorMessage = "Cannot validate argument on parameter 'Path'. " + ($mockErrorMessage -f (Get-TemporaryFolder)) + + { New-SqlDscAudit @mockNewSqlDscAuditParameters } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + + Context 'When passing file audit optional parameters MaximumFileSize and MaximumFileSizeUnit' { + BeforeAll { + $script:mockCreateAuditObject = $null + + Mock -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Audit' + } -MockWith { + <# + The Audit object is created in the script scope so that the + properties can be validated. + #> + $script:mockCreateAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $PesterBoundParameters.ArgumentList[0], + $PesterBoundParameters.ArgumentList[1] + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { + $script:mockMethodCreateCallCount += 1 + } -PassThru -Force + + return $script:mockCreateAuditObject + } + + Mock -CommandName Get-SqlDscAudit + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + Path = Get-TemporaryFolder + Force = $true + } + } + + BeforeEach { + $script:mockMethodCreateCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -MaximumFileSize 1000 -MaximumFileSizeUnit 'Megabyte' @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'File' + $mockCreateAuditObject.FilePath | Should -Be (Get-TemporaryFolder) + $mockCreateAuditObject.MaximumFileSize | Should -Be 1000 + $mockCreateAuditObject.MaximumFileSizeUnit | Should -Be 'Mb' + + $mockMethodCreateCallCount | Should -Be 1 + } + } + + Context 'When passing file audit optional parameters MaximumFiles' { + BeforeAll { + $script:mockCreateAuditObject = $null + + Mock -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Audit' + } -MockWith { + <# + The Audit object is created in the script scope so that the + properties can be validated. + #> + $script:mockCreateAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $PesterBoundParameters.ArgumentList[0], + $PesterBoundParameters.ArgumentList[1] + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { + $script:mockMethodCreateCallCount += 1 + } -PassThru -Force + + return $script:mockCreateAuditObject + } + + Mock -CommandName Get-SqlDscAudit + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + Path = Get-TemporaryFolder + Force = $true + } + } + + BeforeEach { + $script:mockMethodCreateCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -MaximumFiles 2 @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'File' + $mockCreateAuditObject.FilePath | Should -Be (Get-TemporaryFolder) + $mockCreateAuditObject.MaximumFiles | Should -Be 2 + + $mockMethodCreateCallCount | Should -Be 1 + } + } + + Context 'When passing file audit optional parameters MaximumFiles and ReserveDiskSpace' { + BeforeAll { + $script:mockCreateAuditObject = $null + + Mock -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Audit' + } -MockWith { + <# + The Audit object is created in the script scope so that the + properties can be validated. + #> + $script:mockCreateAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $PesterBoundParameters.ArgumentList[0], + $PesterBoundParameters.ArgumentList[1] + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { + $script:mockMethodCreateCallCount += 1 + } -PassThru -Force + + return $script:mockCreateAuditObject + } + + Mock -CommandName Get-SqlDscAudit + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + Path = Get-TemporaryFolder + Force = $true + } + } + + BeforeEach { + $script:mockMethodCreateCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -MaximumFiles 2 -ReserveDiskSpace @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'File' + $mockCreateAuditObject.FilePath | Should -Be (Get-TemporaryFolder) + $mockCreateAuditObject.MaximumFiles | Should -Be 2 + $mockCreateAuditObject.ReserveDiskSpace | Should -BeTrue + + $mockMethodCreateCallCount | Should -Be 1 + } + + Context 'When ReserveDiskSpace is set to $false' { + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -MaximumFiles 2 -ReserveDiskSpace:$false @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'File' + $mockCreateAuditObject.FilePath | Should -Be (Get-TemporaryFolder) + $mockCreateAuditObject.MaximumFiles | Should -Be 2 + $mockCreateAuditObject.ReserveDiskSpace | Should -BeFalse + + $mockMethodCreateCallCount | Should -Be 1 + } + } + } + + Context 'When passing file audit optional parameters MaximumRolloverFiles' { + BeforeAll { + $script:mockCreateAuditObject = $null + + Mock -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Audit' + } -MockWith { + <# + The Audit object is created in the script scope so that the + properties can be validated. + #> + $script:mockCreateAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $PesterBoundParameters.ArgumentList[0], + $PesterBoundParameters.ArgumentList[1] + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { + $script:mockMethodCreateCallCount += 1 + } -PassThru -Force + + return $script:mockCreateAuditObject + } + + Mock -CommandName Get-SqlDscAudit + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + Path = Get-TemporaryFolder + Force = $true + } + } + + BeforeEach { + $script:mockMethodCreateCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -MaximumRolloverFiles 2 @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'File' + $mockCreateAuditObject.FilePath | Should -Be (Get-TemporaryFolder) + $mockCreateAuditObject.MaximumRolloverFiles | Should -Be 2 + + $mockMethodCreateCallCount | Should -Be 1 + } + } + + Context 'When passing audit optional parameter AuditGuid' { + BeforeAll { + $script:mockCreateAuditObject = $null + + Mock -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Audit' + } -MockWith { + <# + The Audit object is created in the script scope so that the + properties can be validated. + #> + $script:mockCreateAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $PesterBoundParameters.ArgumentList[0], + $PesterBoundParameters.ArgumentList[1] + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { + $script:mockMethodCreateCallCount += 1 + } -PassThru -Force + + return $script:mockCreateAuditObject + } + + Mock -CommandName Get-SqlDscAudit + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + Path = Get-TemporaryFolder + Force = $true + } + } + + BeforeEach { + $script:mockMethodCreateCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -AuditGuid 'b5962b93-a359-42ef-bf1e-193e8a5f6222' @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'File' + $mockCreateAuditObject.FilePath | Should -Be (Get-TemporaryFolder) + $mockCreateAuditObject.Guid | Should -Be 'b5962b93-a359-42ef-bf1e-193e8a5f6222' + + $mockMethodCreateCallCount | Should -Be 1 + } + + Context 'When passing an invalid GUID' { + It 'Should throw the correct error' { + $mockErrorMessage = 'Cannot validate argument on parameter ''AuditGuid''. The argument "not a guid" does not match the "^[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$" pattern.' + + # Escape bracket so that Should -Throw works. + $mockErrorMessage = $mockErrorMessage -replace '\[', '`[' + + { New-SqlDscAudit -AuditGuid 'not a guid' @mockDefaultParameters } | + Should -Throw -ExpectedMessage ($mockErrorMessage + '*') + } + } + } + + Context 'When passing audit optional parameter OnFailure' { + BeforeAll { + $script:mockCreateAuditObject = $null + + Mock -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Audit' + } -MockWith { + <# + The Audit object is created in the script scope so that the + properties can be validated. + #> + $script:mockCreateAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $PesterBoundParameters.ArgumentList[0], + $PesterBoundParameters.ArgumentList[1] + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { + $script:mockMethodCreateCallCount += 1 + } -PassThru -Force + + return $script:mockCreateAuditObject + } + + Mock -CommandName Get-SqlDscAudit + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + Path = Get-TemporaryFolder + Force = $true + } + } + + BeforeEach { + $script:mockMethodCreateCallCount = 0 + } + + Context 'When passing the value ' -ForEach @( + @{ + MockOnFailureValue = 'Continue' + } + @{ + MockOnFailureValue = 'FailOperation' + } + @{ + MockOnFailureValue = 'ShutDown' + } + ) { + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -OnFailure $MockOnFailureValue @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'File' + $mockCreateAuditObject.FilePath | Should -Be (Get-TemporaryFolder) + $mockCreateAuditObject.OnFailure | Should -Be $MockOnFailureValue + + $mockMethodCreateCallCount | Should -Be 1 + } + } + } + + Context 'When passing audit optional parameter QueueDelay' { + BeforeAll { + $script:mockCreateAuditObject = $null + + Mock -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Audit' + } -MockWith { + <# + The Audit object is created in the script scope so that the + properties can be validated. + #> + $script:mockCreateAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $PesterBoundParameters.ArgumentList[0], + $PesterBoundParameters.ArgumentList[1] + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { + $script:mockMethodCreateCallCount += 1 + } -PassThru -Force + + return $script:mockCreateAuditObject + } + + Mock -CommandName Get-SqlDscAudit + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + Path = Get-TemporaryFolder + Force = $true + } + } + + BeforeEach { + $script:mockMethodCreateCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -QueueDelay 1000 @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'File' + $mockCreateAuditObject.FilePath | Should -Be (Get-TemporaryFolder) + $mockCreateAuditObject.QueueDelay | Should -Be 1000 + + $mockMethodCreateCallCount | Should -Be 1 + } + } + + Context 'When passing audit optional parameter Filter' { + BeforeAll { + $script:mockCreateAuditObject = $null + + Mock -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Audit' + } -MockWith { + <# + The Audit object is created in the script scope so that the + properties can be validated. + #> + $script:mockCreateAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $PesterBoundParameters.ArgumentList[0], + $PesterBoundParameters.ArgumentList[1] + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { + $script:mockMethodCreateCallCount += 1 + } -PassThru -Force + + return $script:mockCreateAuditObject + } + + Mock -CommandName Get-SqlDscAudit + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + Path = Get-TemporaryFolder + Force = $true + } + } + + BeforeEach { + $script:mockMethodCreateCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + New-SqlDscAudit -AuditFilter "([server_principal_name] like '%ADMINISTRATOR'" @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockCreateAuditObject.Name | Should -Be 'Log1' + $mockCreateAuditObject.DestinationType | Should -Be 'File' + $mockCreateAuditObject.FilePath | Should -Be (Get-TemporaryFolder) + $mockCreateAuditObject.Filter | Should -Be "([server_principal_name] like '%ADMINISTRATOR'" + + $mockMethodCreateCallCount | Should -Be 1 + } + } + + Context 'When passing optional parameter PassThru' { + BeforeAll { + Mock -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Audit' + } -MockWith { + $mockNewCreateAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $PesterBoundParameters.ArgumentList[0], + $PesterBoundParameters.ArgumentList[1] + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { + $script:mockMethodCreateCallCount += 1 + } -PassThru -Force + + return $mockNewCreateAuditObject + } + + Mock -CommandName Get-SqlDscAudit + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + Path = Get-TemporaryFolder + Force = $true + } + } + + BeforeEach { + $script:mockMethodCreateCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + $newSqlDscAuditResult = New-SqlDscAudit -PassThru @mockDefaultParameters + + $newSqlDscAuditResult.Name | Should -Be 'Log1' + $newSqlDscAuditResult.DestinationType | Should -Be 'File' + $newSqlDscAuditResult.FilePath | Should -Be (Get-TemporaryFolder) + + $mockMethodCreateCallCount | Should -Be 1 + } + } + + Context 'When the audit already exist' { + BeforeAll { + Mock -CommandName Get-SqlDscAudit -MockWith { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' + } + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + LogType = 'ApplicationLog' + Force = $true + } + } + + It 'Should throw the correct error' { + $mockErrorMessage = InModuleScope -ScriptBlock { + $script:localizedData.Audit_AlreadyPresent + } + + { New-SqlDscAudit @mockDefaultParameters } | + Should -Throw -ExpectedMessage ($mockErrorMessage -f 'Log1') + } + } +} diff --git a/tests/Unit/Public/Remove-SqlDscAudit.Tests.ps1 b/tests/Unit/Public/Remove-SqlDscAudit.Tests.ps1 new file mode 100644 index 000000000..127027636 --- /dev/null +++ b/tests/Unit/Public/Remove-SqlDscAudit.Tests.ps1 @@ -0,0 +1,190 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Remove-SqlDscAudit' -Tag 'Public' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + MockParameterSetName = 'ServerObject' + MockExpectedParameters = '-ServerObject -Name [-Force] [-Refresh] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'AuditObject' + MockExpectedParameters = '-AuditObject [-Force] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Remove-SqlDscAudit').ParameterSets | + Where-Object -FilterScript { + $_.Name -eq $mockParameterSetName + } | + Select-Object -Property @( + @{ + Name = 'ParameterSetName' + Expression = { $_.Name } + }, + @{ + Name = 'ParameterListAsString' + Expression = { $_.ToString() } + } + ) + + $result.ParameterSetName | Should -Be $MockParameterSetName + $result.ParameterListAsString | Should -Be $MockExpectedParameters + } + + Context 'When removing an audit by ServerObject' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + Mock -CommandName Get-SqlDscAudit -MockWith { + return New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'DropIfExists' -Value { + $script:mockMethodDropIfExistsCallCount += 1 + } -PassThru -Force + } + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + } + } + + BeforeEach { + $script:mockMethodDropIfExistsCallCount = 0 + } + + Context 'When using parameter Confirm with value $false' { + It 'Should call the mocked method and have correct values in the object' { + Remove-SqlDscAudit -Confirm:$false @mockDefaultParameters + + $mockMethodDropIfExistsCallCount | Should -Be 1 + } + } + + Context 'When using parameter Force' { + It 'Should call the mocked method and have correct values in the object' { + Remove-SqlDscAudit -Force @mockDefaultParameters + + $mockMethodDropIfExistsCallCount | Should -Be 1 + } + } + + Context 'When using parameter WhatIf' { + It 'Should call the mocked method and have correct values in the object' { + Remove-SqlDscAudit -WhatIf @mockDefaultParameters + + $mockMethodDropIfExistsCallCount | Should -Be 0 + } + } + + Context 'When passing parameter ServerObject over the pipeline' { + It 'Should call the mocked method and have correct values in the object' { + $mockServerObject | Remove-SqlDscAudit -Name 'Log1' -Force + + $mockMethodDropIfExistsCallCount | Should -Be 1 + } + } + } + + Context 'When removing an audit by AuditObject' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'DropIfExists' -Value { + $script:mockMethodDropIfExistsCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + } + } + + BeforeEach { + $script:mockMethodDropIfExistsCallCount = 0 + } + + Context 'When using parameter Confirm with value $false' { + It 'Should call the mocked method and have correct values in the object' { + Remove-SqlDscAudit -Confirm:$false @mockDefaultParameters + + $mockMethodDropIfExistsCallCount | Should -Be 1 + } + } + + Context 'When using parameter Force' { + It 'Should call the mocked method and have correct values in the object' { + Remove-SqlDscAudit -Force @mockDefaultParameters + + $mockMethodDropIfExistsCallCount | Should -Be 1 + } + } + + Context 'When using parameter WhatIf' { + It 'Should call the mocked method and have correct values in the object' { + Remove-SqlDscAudit -WhatIf @mockDefaultParameters + + $mockMethodDropIfExistsCallCount | Should -Be 0 + } + } + + Context 'When passing parameter ServerObject over the pipeline' { + It 'Should call the mocked method and have correct values in the object' { + $mockAuditObject | Remove-SqlDscAudit -Force + + $mockMethodDropIfExistsCallCount | Should -Be 1 + } + } + } +} diff --git a/tests/Unit/Public/Set-SqlDscAudit.Tests.ps1 b/tests/Unit/Public/Set-SqlDscAudit.Tests.ps1 new file mode 100644 index 000000000..eef5f1098 --- /dev/null +++ b/tests/Unit/Public/Set-SqlDscAudit.Tests.ps1 @@ -0,0 +1,848 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Set-SqlDscAudit' -Tag 'Public' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + MockParameterSetName = 'ServerObject' + MockExpectedParameters = '-ServerObject -Name [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-Path ] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'ServerObjectWithSize' + MockExpectedParameters = '-ServerObject -Name -MaximumFileSize -MaximumFileSizeUnit [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-Path ] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'ServerObjectWithMaxFiles' + MockExpectedParameters = '-ServerObject -Name -MaximumFiles [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-Path ] [-ReserveDiskSpace] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'ServerObjectWithMaxRolloverFiles' + MockExpectedParameters = '-ServerObject -Name -MaximumRolloverFiles [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-Path ] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'ServerObjectWithSizeAndMaxFiles' + MockExpectedParameters = '-ServerObject -Name -MaximumFileSize -MaximumFileSizeUnit -MaximumFiles [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-Path ] [-ReserveDiskSpace] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'ServerObjectWithSizeAndMaxRolloverFiles' + MockExpectedParameters = '-ServerObject -Name -MaximumFileSize -MaximumFileSizeUnit -MaximumRolloverFiles [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-Path ] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'AuditObject' + MockExpectedParameters = '-AuditObject [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-Path ] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'AuditObjectWithSize' + MockExpectedParameters = '-AuditObject -MaximumFileSize -MaximumFileSizeUnit [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-Path ] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'AuditObjectWithMaxFiles' + MockExpectedParameters = '-AuditObject -MaximumFiles [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-Path ] [-ReserveDiskSpace] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'AuditObjectWithMaxRolloverFiles' + MockExpectedParameters = '-AuditObject -MaximumRolloverFiles [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-Path ] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'AuditObjectWithSizeAndMaxFiles' + MockExpectedParameters = '-AuditObject -MaximumFileSize -MaximumFileSizeUnit -MaximumFiles [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-Path ] [-ReserveDiskSpace] [-WhatIf] [-Confirm] []' + } + @{ + MockParameterSetName = 'AuditObjectWithSizeAndMaxRolloverFiles' + MockExpectedParameters = '-AuditObject -MaximumFileSize -MaximumFileSizeUnit -MaximumRolloverFiles [-AuditFilter ] [-OnFailure ] [-QueueDelay ] [-AuditGuid ] [-Force] [-Refresh] [-PassThru] [-Path ] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Set-SqlDscAudit').ParameterSets | + Where-Object -FilterScript { + $_.Name -eq $mockParameterSetName + } | + Select-Object -Property @( + @{ + Name = 'ParameterSetName' + Expression = { $_.Name } + }, + @{ + Name = 'ParameterListAsString' + Expression = { $_.ToString() } + } + ) + + $result.ParameterSetName | Should -Be $MockParameterSetName + $result.ParameterListAsString | Should -Be $MockExpectedParameters + } + + Context 'When setting an audit by an ServerObject' { + BeforeAll { + $script:mockAuditObject = $null + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + Mock -CommandName Get-SqlDscAudit -MockWith { + <# + The Audit object is created in the script scope so that the + properties can be validated. + #> + $script:mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru -Force + + return $script:mockAuditObject + } + + $mockDefaultParameters = @{ + ServerObject = $mockServerObject + Name = 'Log1' + } + } + + BeforeEach { + $script:mockMethodAlterCallCount = 0 + } + + Context 'When using parameter Confirm with value $false' { + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -Confirm:$false -QueueDelay 1000 @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.QueueDelay | Should -Be 1000 + + $mockMethodAlterCallCount | Should -Be 1 + } + } + + Context 'When using parameter Force' { + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -Force -QueueDelay 1000 @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.QueueDelay | Should -Be 1000 + + $mockMethodAlterCallCount | Should -Be 1 + } + } + + Context 'When using parameter WhatIf' { + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -WhatIf -QueueDelay 1000 @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.QueueDelay | Should -BeNullOrEmpty + + $mockMethodAlterCallCount | Should -Be 0 + } + } + + Context 'When passing parameter ServerObject over the pipeline' { + It 'Should call the mocked method and have correct values in the object' { + $mockServerObject | Set-SqlDscAudit -Name 'Log1' -QueueDelay 1000 -Force + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.QueueDelay | Should -Be 1000 + + $mockMethodAlterCallCount | Should -Be 1 + } + } + } + + Context 'When setting an audit by an AuditObject' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + } + + BeforeEach { + $mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + } + + $script:mockMethodAlterCallCount = 0 + } + + Context 'When using parameter Confirm with value $false' { + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -Confirm:$false -QueueDelay 1000 @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.QueueDelay | Should -Be 1000 + + $mockMethodAlterCallCount | Should -Be 1 + } + } + + Context 'When using parameter Force' { + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -Force -QueueDelay 1000 @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.QueueDelay | Should -Be 1000 + + $mockMethodAlterCallCount | Should -Be 1 + } + } + + Context 'When using parameter WhatIf' { + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -WhatIf -QueueDelay 1000 @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.QueueDelay | Should -BeNullOrEmpty + + $mockMethodAlterCallCount | Should -Be 0 + } + } + + Context 'When passing parameter AuditObject over the pipeline' { + It 'Should call the mocked method and have correct values in the object' { + $mockAuditObject | Set-SqlDscAudit -QueueDelay 1000 -Force + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.QueueDelay | Should -Be 1000 + + $mockMethodAlterCallCount | Should -Be 1 + } + } + } + + Context 'When adding an file audit and passing an invalid path' { + BeforeAll { + Mock -CommandName Test-Path -MockWith { + return $false + } + } + + It 'Should throw the correct error' { + $mockNewSqlDscAuditParameters = @{ + ServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + Path = Get-TemporaryFolder + Name = 'Log1' + } + + $mockErrorMessage = InModuleScope -ScriptBlock { + $script:localizedData.Audit_PathParameterValueInvalid + } + + $mockErrorMessage = "Cannot validate argument on parameter 'Path'. " + ($mockErrorMessage -f (Get-TemporaryFolder)) + + { Set-SqlDscAudit @mockNewSqlDscAuditParameters } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + + Context 'When adding an file audit and passing an invalid MaximumFileSize' { + It 'Should throw the correct error when the value is <_>' -ForEach @(1, 2147483648) { + $mockNewSqlDscAuditParameters = @{ + ServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + Name = 'Log1' + MaximumFileSize = $_ + } + + $mockErrorMessage = 'Cannot validate argument on parameter ''MaximumFileSize''. ' + $mockErrorMessage += InModuleScope -ScriptBlock { + $script:localizedData.Audit_MaximumFileSizeParameterValueInvalid + } + + { Set-SqlDscAudit @mockNewSqlDscAuditParameters } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + + Context 'When adding an file audit and passing an invalid QueueDelay' { + It 'Should throw the correct error when the value is <_>' -ForEach @(1, 457, 999, 2147483648) { + $mockNewSqlDscAuditParameters = @{ + ServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + Name = 'Log1' + QueueDelay = $_ + } + + $mockErrorMessage = 'Cannot validate argument on parameter ''QueueDelay''. ' + $mockErrorMessage += InModuleScope -ScriptBlock { + $script:localizedData.Audit_QueueDelayParameterValueInvalid + } + + { Set-SqlDscAudit @mockNewSqlDscAuditParameters } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + + Context 'When passing file audit optional parameter Path' { + BeforeAll { + $script:mockAuditObject = $null + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $script:mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + Force = $true + } + } + + BeforeEach { + $script:mockMethodAlterCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -Path (Get-TemporaryFolder) @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.FilePath | Should -Be (Get-TemporaryFolder) + + $mockMethodAlterCallCount | Should -Be 1 + } + } + + Context 'When passing file audit optional parameters MaximumFileSize and MaximumFileSizeUnit' { + BeforeAll { + $script:mockAuditObject = $null + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $script:mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + Force = $true + } + } + + BeforeEach { + $script:mockMethodAlterCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -MaximumFileSize 1000 -MaximumFileSizeUnit 'Megabyte' @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.MaximumFileSize | Should -Be 1000 + $mockAuditObject.MaximumFileSizeUnit | Should -Be 'Mb' + + $mockMethodAlterCallCount | Should -Be 1 + } + } + + Context 'When passing file audit optional parameters MaximumFiles' { + BeforeAll { + $script:mockAuditObject = $null + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $script:mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + Force = $true + } + } + + BeforeEach { + $script:mockMethodAlterCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -MaximumFiles 2 @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.MaximumFiles | Should -Be 2 + + $mockMethodAlterCallCount | Should -Be 1 + } + } + + Context 'When passing file audit optional parameters MaximumFiles and ReserveDiskSpace' { + BeforeAll { + $script:mockAuditObject = $null + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $script:mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + Force = $true + } + } + + BeforeEach { + $script:mockMethodAlterCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -MaximumFiles 2 -ReserveDiskSpace @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.MaximumFiles | Should -Be 2 + $mockAuditObject.ReserveDiskSpace | Should -BeTrue + + $mockMethodAlterCallCount | Should -Be 1 + } + + Context 'When ReserveDiskSpace is set to $false' { + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -MaximumFiles 2 -ReserveDiskSpace:$false @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.MaximumFiles | Should -Be 2 + $mockAuditObject.ReserveDiskSpace | Should -BeFalse + + $mockMethodAlterCallCount | Should -Be 1 + } + } + } + + Context 'When passing file audit optional parameters MaximumRolloverFiles' { + BeforeAll { + $script:mockAuditObject = $null + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $script:mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + Force = $true + } + } + + BeforeEach { + $script:mockMethodAlterCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -MaximumRolloverFiles 2 @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.MaximumRolloverFiles | Should -Be 2 + + $mockMethodAlterCallCount | Should -Be 1 + } + } + + Context 'When passing audit optional parameter AuditGuid' { + BeforeAll { + $script:mockAuditObject = $null + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $script:mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + Force = $true + } + } + + BeforeEach { + $script:mockMethodAlterCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -AuditGuid 'b5962b93-a359-42ef-bf1e-193e8a5f6222' @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.Guid | Should -Be 'b5962b93-a359-42ef-bf1e-193e8a5f6222' + + $mockMethodAlterCallCount | Should -Be 1 + } + + Context 'When passing an invalid GUID' { + It 'Should throw the correct error' { + $mockErrorMessage = 'Cannot validate argument on parameter ''AuditGuid''. The argument "not a guid" does not match the "^[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$" pattern.' + + # Escape bracket so that Should -Throw works. + $mockErrorMessage = $mockErrorMessage -replace '\[', '`[' + + { Set-SqlDscAudit -AuditGuid 'not a guid' @mockDefaultParameters } | + Should -Throw -ExpectedMessage ($mockErrorMessage + '*') + } + } + } + + Context 'When passing audit optional parameter OnFailure' { + BeforeAll { + $script:mockAuditObject = $null + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $script:mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + Force = $true + } + } + + BeforeEach { + $script:mockMethodAlterCallCount = 0 + } + + Context 'When passing the value ' -ForEach @( + @{ + MockOnFailureValue = 'Continue' + } + @{ + MockOnFailureValue = 'FailOperation' + } + @{ + MockOnFailureValue = 'ShutDown' + } + ) { + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -OnFailure $MockOnFailureValue @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.OnFailure | Should -Be $MockOnFailureValue + + $mockMethodAlterCallCount | Should -Be 1 + } + } + } + + Context 'When passing audit optional parameter QueueDelay' { + BeforeAll { + $script:mockAuditObject = $null + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $script:mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + Force = $true + } + } + + BeforeEach { + $script:mockMethodAlterCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -QueueDelay 1000 @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.QueueDelay | Should -Be 1000 + + $mockMethodAlterCallCount | Should -Be 1 + } + } + + Context 'When passing audit optional parameter Filter' { + BeforeAll { + $script:mockAuditObject = $null + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $script:mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + Force = $true + } + } + + BeforeEach { + $script:mockMethodAlterCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -AuditFilter "([server_principal_name] like '%ADMINISTRATOR'" @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.Filter | Should -Be "([server_principal_name] like '%ADMINISTRATOR'" + + $mockMethodAlterCallCount | Should -Be 1 + } + } + + Context 'When passing optional parameter PassThru' { + BeforeAll { + $script:mockAuditObject = $null + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $script:mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + Force = $true + } + } + + BeforeEach { + $script:mockMethodAlterCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + $newSqlDscAuditResult = Set-SqlDscAudit -QueueDelay 1000 -PassThru @mockDefaultParameters + + $newSqlDscAuditResult.Name | Should -Be 'Log1' + $newSqlDscAuditResult.QueueDelay | Should -Be 1000 + + $mockMethodAlterCallCount | Should -Be 1 + } + } + + Context 'When passing optional parameter Refresh' { + BeforeAll { + $script:mockAuditObject = $null + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $script:mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru | + Add-Member -MemberType 'ScriptMethod' -Name 'Refresh' -Value { + $script:mockMethodRefreshCallCount += 1 + } -PassThru -Force + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + Force = $true + } + } + + BeforeEach { + $script:mockMethodAlterCallCount = 0 + $script:mockMethodRefreshCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + Set-SqlDscAudit -QueueDelay 1000 -Refresh @mockDefaultParameters + + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.QueueDelay | Should -Be 1000 + + $mockMethodAlterCallCount | Should -Be 1 + $mockMethodRefreshCallCount | Should -Be 1 + } + } + + Context 'When switching from MaximumRolloverFiles to MaximumFiles' { + BeforeAll { + $script:mockAuditObject = $null + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $script:mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru -Force + + $script:mockAuditObject.MaximumRolloverFiles = 10 + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + Force = $true + } + } + + BeforeEach { + $script:mockMethodAlterCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + $mockAuditObject.MaximumRolloverFiles | Should -Be 10 -Because 'there has to be a value greater than 0 in the object that is passed to the command in this test' + + Set-SqlDscAudit -MaximumFiles 2 @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.MaximumRolloverFiles | Should -Be 0 + $mockAuditObject.MaximumFiles | Should -Be 2 + + $mockMethodAlterCallCount | Should -Be 2 -Because 'the call to Alter() need to happen twice, first to set MaximumRolloverFiles to 0, then another to set MaximumFiles to the new value' + } + } + + Context 'When switching from MaximumFiles to MaximumRolloverFiles' { + BeforeAll { + $script:mockAuditObject = $null + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $script:mockAuditObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Audit' -ArgumentList @( + $mockServerObject, + 'Log1' + ) | + Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + $script:mockMethodAlterCallCount += 1 + } -PassThru -Force + + $script:mockAuditObject.MaximumFiles = 10 + + $mockDefaultParameters = @{ + AuditObject = $mockAuditObject + Force = $true + } + } + + BeforeEach { + $script:mockMethodAlterCallCount = 0 + } + + It 'Should call the mocked method and have correct values in the object' { + $mockAuditObject.MaximumFiles | Should -Be 10 -Because 'there has to be a value greater than 0 in the object that is passed to the command in this test' + + Set-SqlDscAudit -MaximumRolloverFiles 2 @mockDefaultParameters + + # This is the object created by the mock and modified by the command. + $mockAuditObject.Name | Should -Be 'Log1' + $mockAuditObject.MaximumFiles | Should -Be 0 + $mockAuditObject.MaximumRolloverFiles | Should -Be 2 + + $mockMethodAlterCallCount | Should -Be 2 -Because 'the call to Alter() need to happen twice, first to set MaximumFiles to 0, then another to set MaximumRolloverFiles to the new value' + } + } +} diff --git a/tests/Unit/Stubs/SMO.cs b/tests/Unit/Stubs/SMO.cs index a987f4ff2..1f21e9cea 100644 --- a/tests/Unit/Stubs/SMO.cs +++ b/tests/Unit/Stubs/SMO.cs @@ -123,16 +123,42 @@ public enum EndpointEncryptionAlgorithm RC4Aes } - #endregion Public Enums + public enum AuditDestinationType : int + { + File = 0, + SecurityLog = 1, + ApplicationLog = 2, + Url = 3, + Unknown = 100, + } - #region Public Classes + public enum AuditFileSizeUnit : int + { + Mb = 0, + Gb = 1, + Tb = 2, + } - public class Globals + public enum OnFailureAction : int { - // Static property that is switched on or off by tests if data should be mocked (true) or not (false). - public static bool GenerateMockData = false; + Continue = 0, + Shutdown = 1, + FailOperation = 2, } + public enum SqlSmoState : int + { + Pending = 0, + Creating = 1, + Existing = 2, + ToBeDropped = 3, + Dropped = 4, + } + + #endregion Public Enums + + #region Public Classes + // Typename: Microsoft.SqlServer.Management.Smo.ObjectPermissionSet // BaseType: Microsoft.SqlServer.Management.Smo.PermissionSetBase // Used by: @@ -856,6 +882,46 @@ public Endpoint this[string name] } } + public class Audit + { + // Constructor + public Audit() { } + public Audit(Microsoft.SqlServer.Management.Smo.Server server, System.String name) { + this.Parent = server; + this.Name = name; + } + + // Property + public Microsoft.SqlServer.Management.Smo.Server Parent { get; set; } + public System.DateTime? CreateDate { get; set; } + public System.DateTime? DateLastModified { get; set; } + public Microsoft.SqlServer.Management.Smo.AuditDestinationType? DestinationType { get; set; } + public System.Boolean? Enabled { get; set; } + public System.String FileName { get; set; } + public System.String FilePath { get; set; } + public System.String Filter { get; set; } + public System.Guid? Guid { get; set; } + public System.Int32? ID { get; set; } + public System.Int32? MaximumFiles { get; set; } + public System.Int32? MaximumFileSize { get; set; } + public Microsoft.SqlServer.Management.Smo.AuditFileSizeUnit? MaximumFileSizeUnit { get; set; } + public System.Int64? MaximumRolloverFiles { get; set; } + public Microsoft.SqlServer.Management.Smo.OnFailureAction? OnFailure { get; set; } + public System.Int32? QueueDelay { get; set; } + public System.Boolean? ReserveDiskSpace { get; set; } + public System.Int32? RetentionDays { get; set; } + public System.String Name { get; set; } + // public Microsoft.SqlServer.Management.Smo.AbstractCollectionBase ParentCollection { get; set; } + // public Microsoft.SqlServer.Management.Sdk.Sfc.Urn Urn { get; set; } + // public Microsoft.SqlServer.Management.Smo.SqlPropertyCollection Properties { get; set; } + // public Microsoft.SqlServer.Management.Common.ServerVersion ServerVersion { get; set; } + // public Microsoft.SqlServer.Management.Common.DatabaseEngineType DatabaseEngineType { get; set; } + // public Microsoft.SqlServer.Management.Common.DatabaseEngineEdition DatabaseEngineEdition { get; set; } + // public Microsoft.SqlServer.Management.Smo.ExecutionManager ExecutionManager { get; set; } + public System.Object UserData { get; set; } + public Microsoft.SqlServer.Management.Smo.SqlSmoState? State { get; set; } + } + #endregion Public Classes }