diff --git a/JiraPS/Public/Remove-JiraIssue.ps1 b/JiraPS/Public/Remove-JiraIssue.ps1 new file mode 100644 index 00000000..d9bc1ee4 --- /dev/null +++ b/JiraPS/Public/Remove-JiraIssue.ps1 @@ -0,0 +1,116 @@ +function Remove-JiraIssue { + [CmdletBinding( + ConfirmImpact = 'High', + SupportsShouldProcess, + DefaultParameterSetName = "ByInputObject" + )] + param ( + [Parameter( + Mandatory, + ValueFromPipeline, + Position = 0, + ParameterSetName = "ByInputObject" + )] + [Alias( + "Issue" + )] + [PSTypeName("JiraPS.Issue")] + [Object[]] + $InputObject, + + # The issue's ID number or key. + [Parameter( + Mandatory, + Position = 0, + ParameterSetName = "ByIssueId" + )] + [ValidateNotNullOrEmpty()] + [Alias( + "Id", + "Key", + "issueIdOrKey" + )] + [String[]] + $IssueId, + + [Switch] + [Alias("deleteSubtasks")] + $IncludeSubTasks, + + [System.Management.Automation.CredentialAttribute()] + [System.Management.Automation.PSCredential] + $Credential = [System.Management.Automation.PSCredential]::Empty, + + [Switch] + $Force + ) + + begin { + Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function started" + + $server = Get-JiraConfigServer -ErrorAction Stop + + $resourceURi = "$server/rest/api/latest/issue/{0}?deleteSubtasks={1}" + + if ($Force) { + Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] -Force was passed. Backing up current ConfirmPreference [$ConfirmPreference] and setting to None" + $oldConfirmPreference = $ConfirmPreference + $ConfirmPreference = 'None' + } + } + + process { + + Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] ParameterSetName: $($PsCmdlet.ParameterSetName)" + Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] PSBoundParameters: $($PSBoundParameters | Out-String)" + + switch ($PsCmdlet.ParameterSetName) { + "ByInputObject" { $PrimaryIterator = $InputObject } + "ByIssueId" { $PrimaryIterator = $IssueID } + } + + foreach ($issueItem in $PrimaryIterator) { + + if ($PsCmdlet.ParameterSetName -eq "ByIssueId") { + $_issue = Get-JiraIssue -Key $issueItem -Credential $Credential -ErrorAction Stop + } Else { + $_issue = $issueItem + } + + Write-Verbose "[$($MyInvocation.MyCommand.Name)] Processing [$_issue]" + Write-Debug "[$($MyInvocation.MyCommand.Name)] Processing `$issueItem [$_issue]" + + + + $parameter = @{ + URI = $resourceURi -f $_issue.Key,$IncludeSubTasks + Method = "DELETE" + Credential = $Credential + Cmdlet = $PsCmdlet + } + + + If ($IncludeSubTasks) { + $ActionText = "Remove issue and sub-tasks" + } Else { + $ActionText = "Remove issue" + } + + if ($PSCmdlet.ShouldProcess($_issue.ToString(), $ActionText)) { + + Write-Debug "[$($MyInvocation.MyCommand.Name)] Invoking JiraMethod with `$parameter" + Invoke-JiraMethod @parameter + } + } + + } + + end { + if ($Force) { + Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Restoring ConfirmPreference to [$oldConfirmPreference]" + $ConfirmPreference = $oldConfirmPreference + } + + Write-Verbose "[$($MyInvocation.MyCommand.Name)] Complete" + } +} diff --git a/Tests/Remove-JiraIssue.Tests.ps1 b/Tests/Remove-JiraIssue.Tests.ps1 new file mode 100644 index 00000000..b17143a3 --- /dev/null +++ b/Tests/Remove-JiraIssue.Tests.ps1 @@ -0,0 +1,290 @@ +Describe "Remove-JiraIssue" { + Import-Module "$PSScriptRoot/../JiraPS" -Force -ErrorAction Stop + + InModuleScope JiraPS { + + . "$PSScriptRoot/Shared.ps1" + + $jiraServer = 'http://jiraserver.example.com' + + $TestIssueJSONs = @{ + + # basic issue + 'TEST-1' = @' + { + "expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations", + "id": "58159", + "self": "https://jiraserver.example.com/rest/api/2/issue/58159", + "key": "TEST-1", + "fields": { + "subtasks": [], + "project": { + "self": "https://jiraserver.example.com/rest/api/2/project/14801", + "id": "14801", + "key": "TEST", + "name": "TEST - Service Desk", + "avatarUrls": { + "48x48": "https://jiraserver.example.com/secure/projectavatar?avatarId=12003", + "24x24": "https://jiraserver.example.com/secure/projectavatar?size=small&avatarId=12003", + "16x16": "https://jiraserver.example.com/secure/projectavatar?size=xsmall&avatarId=12003", + "32x32": "https://jiraserver.example.com/secure/projectavatar?size=medium&avatarId=12003" + } + }, + "aggregatetimespent": null, + "resolutiondate": null, + "workratio": -1, + "description": "Test issue.", + "summary": "Test Issue", + "comment": { + "comments": [], + "maxResults": 0, + "total": 0, + "startAt": 0 + } + } + } +'@ + # issue w/ subtasks + 'TEST-2' = @' + { + "expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations", + "id": "58160", + "self": "https://jiraserver.example.com/rest/api/2/issue/58160", + "key": "TEST-2", + "fields": { + "subtasks": [ + { + "id": "58161", + "key": "TEST-3", + "self": "https://jiraserver.example.com/rest/api/2/issue/58161", + "fields": { + "summary": "Test Sub-Task", + "status": { + "self": "https://jiraserver.example.com/rest/api/2/status/11202", + "description": "This was auto-generated by JIRA Service Desk during workflow import", + "iconUrl": "https://jiraserver.example.com/images/icons/status_generic.gif", + "name": "Open", + "id": "11202", + "statusCategory": { + "self": "https://jiraserver.example.com/rest/api/2/statuscategory/2", + "id": 2, + "key": "new", + "colorName": "blue-gray", + "name": "To Do" + } + }, + "priority": { + "self": "https://jiraserver.example.com/rest/api/2/priority/4", + "iconUrl": "https://jiraserver.example.com/images/icons/priorities/minor.svg", + "name": "Medium", + "id": "4" + }, + "issuetype": { + "self": "https://jiraserver.example.com/rest/api/2/issuetype/5", + "id": "5", + "description": "The sub-task of the issue", + "iconUrl": "https://jiraserver.example.com/secure/viewavatar?size=xsmall&avatarId=11016&avatarType=issuetype", + "name": "Sub-task", + "subtask": true, + "avatarId": 11016 + } + } + } + ], + "project": { + "self": "https://jiraserver.example.com/rest/api/2/project/14801", + "id": "14801", + "key": "TEST", + "name": "TEST - Service Desk", + "avatarUrls": { + "48x48": "https://jiraserver.example.com/secure/projectavatar?avatarId=12003", + "24x24": "https://jiraserver.example.com/secure/projectavatar?size=small&avatarId=12003", + "16x16": "https://jiraserver.example.com/secure/projectavatar?size=xsmall&avatarId=12003", + "32x32": "https://jiraserver.example.com/secure/projectavatar?size=medium&avatarId=12003" + } + }, + "description": "Test issue with a sub-task attached.", + "summary": "Test Parent-Task Issue", + "comment": { + "comments": [], + "maxResults": 0, + "total": 0, + "startAt": 0 + } + } + } +'@ + # the sub-task itself + 'TEST-3' = @' + { + "expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations", + "id": "58161", + "self": "https://jiraserver.example.com/rest/api/2/issue/58161", + "key": "TEST-3", + "fields": { + "parent": { + "id": "58160", + "key": "TEST-2", + "self": "https://jiraserver.example.com/rest/api/2/issue/58160", + "fields": { + "summary": "Test Parent-Task Issue", + "status": { + "self": "https://jiraserver.example.com/rest/api/2/status/1", + "description": "The issue is new and has not been looked at yet.", + "iconUrl": "https://jiraserver.example.com/images/icons/statuses/open.png", + "name": "OPENED", + "id": "1", + "statusCategory": { + "self": "https://jiraserver.example.com/rest/api/2/statuscategory/2", + "id": 2, + "key": "new", + "colorName": "blue-gray", + "name": "To Do" + } + }, + "priority": { + "self": "https://jiraserver.example.com/rest/api/2/priority/4", + "iconUrl": "https://jiraserver.example.com/images/icons/priorities/minor.svg", + "name": "Medium", + "id": "4" + }, + "issuetype": { + "self": "https://jiraserver.example.com/rest/api/2/issuetype/3", + "id": "3", + "description": "A task that needs to be done.", + "iconUrl": "https://jiraserver.example.com/secure/viewavatar?size=xsmall&avatarId=11018&avatarType=issuetype", + "name": "Task", + "subtask": false, + "avatarId": 11018 + } + } + }, + "subtasks": [], + "project": { + "self": "https://jiraserver.example.com/rest/api/2/project/14801", + "id": "14801", + "key": "TEST", + "name": "TEST - Service Desk", + "avatarUrls": { + "48x48": "https://jiraserver.example.com/secure/projectavatar?avatarId=12003", + "24x24": "https://jiraserver.example.com/secure/projectavatar?size=small&avatarId=12003", + "16x16": "https://jiraserver.example.com/secure/projectavatar?size=xsmall&avatarId=12003", + "32x32": "https://jiraserver.example.com/secure/projectavatar?size=medium&avatarId=12003" + } + }, + "description": "Test sub-task.", + "summary": "Test Sub-Task", + "comment": { + "comments": [], + "maxResults": 0, + "total": 0, + "startAt": 0 + } + } + } +'@ + + } + + Mock Get-JiraConfigServer -ModuleName JiraPS { + Write-Output $jiraServer + } + + Mock Get-JiraIssue { + $obj = $TestIssueJSONs[$Key] | ConvertFrom-Json + + $obj.PSObject.TypeNames.Insert(0, 'JiraPS.Issue') + + $obj | Add-Member -MemberType ScriptMethod -Name ToString -Value {return ""} -Force + return $obj + } + + Mock Invoke-JiraMethod -ModuleName JiraPS -ParameterFilter {$URI -like "$jiraServer/rest/api/*/issue/TEST-1?*" -and $Method -eq "Delete"} { + return $null + } + + Mock Invoke-JiraMethod -ModuleName JiraPS -ParameterFilter {$URI -like "$jiraServer/rest/api/*/issue/TEST-2?deleteSubTasks=False" -and $Method -eq "Delete"} { + + Write-Error -Exception -ErrorId + $MockedResponse = @" + { + "errorMessages": [ + "The issue 'TEST-2' has subtasks. You must specify the 'deleteSubtasks' parameter to delete this issue and all its subtasks." + ], + "errors": {} + } +"@ | ConvertFrom-Json + + + $Exception = ([System.ArgumentException]"Server responded with Error") + $errorId = "ServerResponse" + $errorCategory = 'NotSpecified' + $errorTarget = $MockedResponse + + $errorItem = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $Exception,$errorId,$errorCategory,$errorTarget + $errorItem.ErrorDetails = "Jira encountered an error: [The issue 'TEST-2' has subtasks. You must specify the 'deleteSubtasks' parameter to delete this issue and all its subtasks.]" + + $PSCmdlet.WriteError($errorItem) + } + + Mock Invoke-JiraMethod -ModuleName JiraPS -ParameterFilter {$URI -like "$jiraServer/rest/api/*/issue/TEST-2?deleteSubTasks=True" -and $Method -eq "Delete"} { + return $null + } + + # Generic catch-all. This will throw an exception if we forgot to mock something. + Mock Invoke-JiraMethod -ModuleName JiraPS { + ShowMockInfo 'Invoke-JiraMethod' 'Method', 'Uri' + throw "Unidentified call to Invoke-JiraMethod" + } + + ############# + # Tests + ############# + + Context "Sanity checking" { + $command = Get-Command -Name Remove-JiraIssue + + defParam $command 'IssueId' + defParam $command 'InputObject' + defParam $command 'IncludeSubTasks' + defParam $command 'Credential' + } + + Context "Functionality" { + + It "Accepts generic object with the correct properties" { + { + $issue = Get-JiraIssue -Key TEST-1 + Remove-JiraIssue -Issue $issue -Force + } | Should Not Throw + Assert-MockCalled -CommandName Invoke-JiraMethod -Exactly -Times 1 -Scope It + } + + It "Accepts string-based input as a non-pipelined parameter" { + {Remove-JiraIssue -IssueId TEST-1 -Force} | Should Not Throw + Assert-MockCalled -CommandName Invoke-JiraMethod -Exactly -Times 1 -Scope It + } + + It "Accepts a JiraPS.Issue object over the pipeline" { + { Get-JiraIssue -Key TEST-1 | Remove-JiraIssue -Force} | Should Not Throw + Assert-MockCalled -CommandName Invoke-JiraMethod -Exactly -Times 1 -Scope It + } + + It "Writes an error on issues with subtasks" { + # Pester is not capable of (easily) asserting non-terminating errors, + # so the error is upgraded to a terminating one in this situation. + { Get-JiraIssue -Key TEST-2 | Remove-JiraIssue -Force -ErrorAction Stop} | Should Throw + Assert-MockCalled -CommandName Invoke-JiraMethod -Exactly -Times 1 -Scope It + } + + It "Passes on issues with subtasks and -DeleteSubTasks" { + { Get-JiraIssue -Key TEST-2 | Remove-JiraIssue -IncludeSubTasks -Force} | Should Not Throw + Assert-MockCalled -CommandName Invoke-JiraMethod -Exactly -Times 1 -Scope It + } + + It "Validates pipeline input" { + { @{id = 1} | Remove-JiraIssue -ErrorAction Stop} | Should Throw + } + } + } +} diff --git a/docs/en-US/commands/Remove-JiraIssue.md b/docs/en-US/commands/Remove-JiraIssue.md new file mode 100644 index 00000000..f4e3c64e --- /dev/null +++ b/docs/en-US/commands/Remove-JiraIssue.md @@ -0,0 +1,208 @@ +--- +external help file: JiraPS-help.xml +Module Name: JiraPS +online version: https://atlassianps.org/docs/JiraPS/commands/Remove-JiraIssue/ +locale: en-US +schema: 2.0.0 +layout: documentation +permalink: /docs/JiraPS/commands/Remove-JiraIssue/ +--- + +# Remove-JiraIssue + +## SYNOPSIS + +Removes an existing issue from JIRA. + +## SYNTAX + +### ByInputObject (Default) + +```powershell +Remove-JiraIssue [-InputObject] [-IncludeSubTasks] [[-Credential] ] [-Force] [-WhatIf] + [-Confirm] [] +``` + +### ByIssueId + +```powershell +Remove-JiraIssue [-IssueId] [-IncludeSubTasks] [[-Credential] ] [-Force] [-WhatIf] + [-Confirm] [] +``` + +## DESCRIPTION + +This function will remove an issue from Jira. +Deleting an issue removes it permanently from JIRA, including all of its comments and attachments. + +If you have completed an issue, it should usually be resolved or closed - not deleted. + +If an issue includes sub-tasks, these are deleted as well. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Remove-JiraIssue -IssueId ABC-123 +``` + +Removes issue \[ABC-123\] from JIRA. + +### EXAMPLE 2 + +```powershell +Remove-JiraIssue -IssueId ABC-124 -IncludeSubTasks +``` + +Removes issue \[ABC-124\] from JIRA, including any subtasks therein. + +### EXAMPLE 3 + +```powershell +Get-JiraIssue -Query "Project = ABC AND label = NeedsDeletion" | Remove-JiraIssue -IncludeSubTasks +``` + +Removes all issues from project ABC (including their subtasks) that have the label "NeedsDeletion". + +## PARAMETERS + +### -InputObject + +One or more issues to delete, specified as `JiraPS.Issue` objects (e.g. from `Get-JiraIssue`) + +```yaml +Type: Object[] +Parameter Sets: ByInputObject +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -IssueId + +One or more issues to delete, either: + +* Issue Keys (e.g. "TEST-1234") +* Numerical issue IDs + +```yaml +Type: String[] +Parameter Sets: ByInputObject +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -IncludeSubTasks + +Removes any subtasks associated with the issue(s) to be deleted. + +If the issue has no subtasks, this parameter is ignored. If the issue has subtasks and this parameter is missing, then the issue will not be deleted and an error will be returned. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Credential + +Credentials to use to connect to JIRA. + +```yaml +Type: PSCredential +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force + +Suppress user confirmation. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -WhatIf + +Shows what would happen if the cmdlet runs. +The cmdlet is not run. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Confirm + +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. +For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### [JiraPS.Issue] / [String] + +## OUTPUTS + +### Output (if any) + +## NOTES + +If the issue has subtasks you must include the parameter IncludeSubTasks to delete the issue. You cannot delete an issue without its subtasks also being deleted. + +This function requires either the \`-Credential\` parameter to be passed or a persistent JIRA session. +See \`New-JiraSession\` for more details. + +## RELATED LINKS