diff --git a/.vscode/tasks.cmd b/.vscode/tasks.cmd new file mode 100644 index 00000000..6bbe21f1 --- /dev/null +++ b/.vscode/tasks.cmd @@ -0,0 +1,9 @@ +@rem Do not edit! This file is generated by New-VSCodeTask.ps1 +@echo off +if "%1" == "!" goto start +chcp 65001 > nul +PowerShell.exe -NoProfile -ExecutionPolicy Bypass "& 'D:\Documents\Projects\PSJira\Build\Invoke-Build.ps1' -File 'D:\Documents\Projects\PSJira\Build\PSJira.build.ps1' %1" +exit +:start +shift +start PowerShell.exe -NoExit -NoProfile -ExecutionPolicy Bypass "& 'D:\Documents\Projects\PSJira\Build\Invoke-Build.ps1' -File 'D:\Documents\Projects\PSJira\Build\PSJira.build.ps1' %1" diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..ca907248 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,29 @@ +// Do not edit! This file is generated by New-VSCodeTask.ps1 +// Modify the build script instead and regenerate this file. +{ + "version": "0.1.0", + "command": ".\\.vscode\\tasks.cmd", + "suppressTaskName": false, + "showOutput": "always", + "tasks": [ + { + "isBuildCommand": true, + "taskName": "." + }, + { + "taskName": "Init" + }, + { + "taskName": "Test" + }, + { + "taskName": "Build" + }, + { + "taskName": "Deploy" + }, + { + "taskName": "?" + } + ] +} diff --git a/PSJira/Internal/ConvertTo-JiraEditMetaField.Tests.ps1 b/PSJira/Internal/ConvertTo-JiraEditMetaField.Tests.ps1 new file mode 100644 index 00000000..de77811e --- /dev/null +++ b/PSJira/Internal/ConvertTo-JiraEditMetaField.Tests.ps1 @@ -0,0 +1,114 @@ +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".") +. "$here\$sut" + +InModuleScope PSJira { + Describe "ConvertTo-JiraEditMetaField" { + function defProp($obj, $propName, $propValue) + { + It "Defines the '$propName' property" { + $obj.$propName | Should Be $propValue + } + } + + $sampleJson = @' +{ + "fields": { + "summary": { + "required": true, + "schema": { + "type": "string", + "system": "summary" + }, + "name": "Summary", + "hasDefaultValue": false, + "operations": [ + "set" + ] + }, + "priority": { + "required": false, + "schema": { + "type": "priority", + "system": "priority" + }, + "name": "Priority", + "hasDefaultValue": true, + "operations": [ + "set" + ], + "allowedValues": [ + { + "self": "http://jiraserver.example.com/rest/api/2/priority/1", + "iconUrl": "http://jiraserver.example.com/images/icons/priorities/blocker.png", + "name": "Block", + "id": "1" + }, + { + "self": "http://jiraserver.example.com/rest/api/2/priority/2", + "iconUrl": "http://jiraserver.example.com/images/icons/priorities/critical.png", + "name": "Critical", + "id": "2" + }, + { + "self": "http://jiraserver.example.com/rest/api/2/priority/3", + "iconUrl": "http://jiraserver.example.com/images/icons/priorities/major.png", + "name": "Major", + "id": "3" + }, + { + "self": "http://jiraserver.example.com/rest/api/2/priority/4", + "iconUrl": "http://jiraserver.example.com/images/icons/priorities/minor.png", + "name": "Minor", + "id": "4" + }, + { + "self": "http://jiraserver.example.com/rest/api/2/priority/5", + "iconUrl": "http://jiraserver.example.com/images/icons/priorities/trivial.png", + "name": "Trivial", + "id": "5" + } + ] + } + } +} +'@ + $sampleObject = ConvertFrom-Json2 -InputObject $sampleJson + + $r = ConvertTo-JiraEditMetaField $sampleObject + + It "Creates PSObjects out of JSON input" { + $r | Should Not BeNullOrEmpty + $r.Count | Should Be 2 + } + + It "Sets the type name to PSJira.CreateMetaField" { + # Need to use the pipeline in this case, instead of directly using the + # -InputObject parameter. This is a quirk of PowerShell, arrays, and + # the pipeline. + ($r | Get-Member).TypeName | Should Be 'PSJira.EditMetaField' + } + + Context "Data validation" { + # Our sample JSON includes two fields: summary and priority. + $summary = ConvertTo-JiraEditMetaField $sampleObject | Where-Object -FilterScript {$_.Name -eq 'Summary'} + $priority = ConvertTo-JiraEditMetaField $sampleObject | Where-Object -FilterScript {$_.Name -eq 'Priority'} + + defProp $summary 'Id' 'summary' + defProp $summary 'Name' 'Summary' + defProp $summary 'HasDefaultValue' $false + defProp $summary 'Required' $true + defProp $summary 'Operations' @('set') + + It "Defines the 'Schema' property if available" { + $summary.Schema | Should Not BeNullOrEmpty + $priority.Schema | Should Not BeNullOrEmpty + } + + It "Defines the 'AllowedValues' property if available" { + $summary.AllowedValues | Should BeNullOrEmpty + $priority.AllowedValues | Should Not BeNullOrEmpty + } + } + } +} diff --git a/PSJira/Internal/ConvertTo-JiraEditMetaField.ps1 b/PSJira/Internal/ConvertTo-JiraEditMetaField.ps1 new file mode 100644 index 00000000..958a2ef0 --- /dev/null +++ b/PSJira/Internal/ConvertTo-JiraEditMetaField.ps1 @@ -0,0 +1,99 @@ +function ConvertTo-JiraEditMetaField +{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, + Position = 0, + ValueFromPipeline = $true)] + [PSObject[]] $InputObject, + + [Switch] $ReturnError + ) + + process + { + foreach ($i in $InputObject) + { + Write-Debug "[ConvertTo-JiraEditMetaField] Processing object: '$i'" + + if ($i.errorMessages) + { +# Write-Debug "[ConvertTo-JiraEditMetaField] Detected an errorMessages property. This is an error result." + + if ($ReturnError) + { +# Write-Debug "[ConvertTo-JiraEditMetaField] Outputting details about error message" + $props = @{ + 'ErrorMessages' = $i.errorMessages; + } + + $result = New-Object -TypeName PSObject -Property $props + $result.PSObject.TypeNames.Insert(0, 'PSJira.Error') + + Write-Output $result + } + } else { + $fields = $i.fields + $fieldNames = (Get-Member -InputObject $fields -MemberType '*Property').Name + foreach ($f in $fieldNames) + { + Write-Debug "[ConvertTo-JiraEditMetaField] Processing field [$f]" + $item = $fields.$f + + $props = @{ + 'Id' = $f; + 'Name' = $item.name; + 'HasDefaultValue' = [System.Convert]::ToBoolean($item.hasDefaultValue); + 'Required' = [System.Convert]::ToBoolean($item.required); + 'Schema' = $item.schema; + 'Operations' = $item.operations; + } + + if ($item.allowedValues) + { +# Write-Debug "[ConvertTo-JiraEditMetaField] Adding AllowedValues" + $props.AllowedValues = $item.allowedValues + } + + if ($item.autoCompleteUrl) + { +# Write-Debug "[ConvertTo-JiraEditMetaField] Adding AutoCompleteURL" + $props.AutoCompleteUrl = $item.autoCompleteUrl + } + +# Write-Debug "[ConvertTo-JiraEditMetaField] Checking for any additional properties" + foreach ($extraProperty in (Get-Member -InputObject $item -MemberType NoteProperty).Name) + { +# Write-Debug "[ConvertTo-JiraEditMetaField] Checking property $extraProperty" + if ($props.$extraProperty -eq $null) + { +# Write-Debug "[ConvertTo-JiraEditMetaField] - Adding property [$extraProperty]" + $props.$extraProperty = $item.$extraProperty + } + } + +# Write-Debug "[ConvertTo-JiraEditMetaField] Creating PSObject out of properties" + $result = New-Object -TypeName PSObject -Property $props + +# Write-Debug "[ConvertTo-JiraEditMetaField] Inserting type name information" + $result.PSObject.TypeNames.Insert(0, 'PSJira.EditMetaField') + +# Write-Debug "[ConvertTo-JiraEditMetaField] Inserting custom toString() method" + $result | Add-Member -MemberType ScriptMethod -Name "ToString" -Force -Value { + Write-Output "$($this.Name)" + } + +# Write-Debug "[ConvertTo-JiraEditMetaField] Outputting object" + Write-Output $result + } + } + } + } + + end + { +# Write-Debug "[ConvertTo-JiraEditMetaField] Complete" + } +} + + diff --git a/PSJira/PSJira.psd1 b/PSJira/PSJira.psd1 index fa968ad6..1777ba8f 100644 --- a/PSJira/PSJira.psd1 +++ b/PSJira/PSJira.psd1 @@ -69,15 +69,15 @@ FormatsToProcess = 'PSJira.format.ps1xml' # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = 'Add-JiraGroupMember', 'Add-JiraIssueComment', 'Format-Jira', - 'Get-JiraConfigServer', 'Get-JiraField', 'Get-JiraFilter', - 'Get-JiraGroup', 'Get-JiraGroupMember', 'Get-JiraIssue', - 'Get-JiraIssueComment', 'Get-JiraIssueCreateMetadata', - 'Get-JiraIssueType', 'Get-JiraPriority', 'Get-JiraProject', - 'Get-JiraSession', 'Get-JiraUser', 'Invoke-JiraIssueTransition', - 'New-JiraGroup', 'New-JiraIssue', 'New-JiraSession', 'New-JiraUser', - 'Remove-JiraGroup', 'Remove-JiraGroupMember', 'Remove-JiraSession', - 'Remove-JiraUser', 'Set-JiraConfigServer', 'Set-JiraIssue', +FunctionsToExport = 'Add-JiraGroupMember', 'Add-JiraIssueComment', 'Format-Jira', + 'Get-JiraConfigServer', 'Get-JiraField', 'Get-JiraFilter', + 'Get-JiraGroup', 'Get-JiraGroupMember', 'Get-JiraIssue', + 'Get-JiraIssueComment', 'Get-JiraIssueCreateMetadata', 'Get-JiraIssueEditMetadata', + 'Get-JiraIssueType', 'Get-JiraPriority', 'Get-JiraProject', + 'Get-JiraSession', 'Get-JiraUser', 'Invoke-JiraIssueTransition', + 'New-JiraGroup', 'New-JiraIssue', 'New-JiraSession', 'New-JiraUser', + 'Remove-JiraGroup', 'Remove-JiraGroupMember', 'Remove-JiraSession', + 'Remove-JiraUser', 'Set-JiraConfigServer', 'Set-JiraIssue', 'Set-JiraIssueLabel', 'Set-JiraUser' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. @@ -122,7 +122,7 @@ PrivateData = @{ # ExternalModuleDependencies = '' } # End of PSData hashtable - + } # End of PrivateData hashtable # HelpInfo URI of this module diff --git a/PSJira/Public/Get-JiraIssueEditMetadata.Tests.ps1 b/PSJira/Public/Get-JiraIssueEditMetadata.Tests.ps1 new file mode 100644 index 00000000..9eea8b3d --- /dev/null +++ b/PSJira/Public/Get-JiraIssueEditMetadata.Tests.ps1 @@ -0,0 +1,252 @@ +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".") +. "$here\$sut" + +InModuleScope PSJira { + + $ShowMockData = $false + $ShowDebugText = $false + + Describe "Get-JiraIssueEditMetadata" { + + if ($ShowDebugText) + { + Mock 'Write-Debug' { + Write-Host " [DEBUG] $Message" -ForegroundColor Yellow + } + } + + $issueID = 41701 + $issueKey = 'IT-3676' + + Mock Get-JiraConfigServer { + 'https://jira.example.com' + } + + # If we don't override this in a context or test, we don't want it to + # actually try to query a JIRA instance + Mock Invoke-JiraMethod -ModuleName PSJira { + if ($ShowMockData) + { + Write-Host " Mocked Invoke-JiraMethod" -ForegroundColor Cyan + Write-Host " [Uri] $Uri" -ForegroundColor Cyan + Write-Host " [Method] $Method" -ForegroundColor Cyan + } + } + + Context "Sanity checking" { + $command = Get-Command -Name Get-JiraIssueEditMetadata + + function defParam($name) + { + It "Has a -$name parameter" { + $command.Parameters.Item($name) | Should Not BeNullOrEmpty + } + } + + defParam 'Issue' + defParam 'Credential' + } + + Context "Behavior testing" { + + $restResult = ConvertFrom-Json2 @' +{ + "fields": { + "summary": { + "required": true, + "schema": { + "type": "string", + "system": "summary" + }, + "name": "Summary", + "hasDefaultValue": false, + "operations": [ + "set" + ] + }, + "issuetype": { + "required": true, + "schema": { + "type": "issuetype", + "system": "issuetype" + }, + "name": "Issue Type", + "hasDefaultValue": false, + "operations": [], + "allowedValues": [ + { + "self": "https://jira.example.com/rest/api/2/issuetype/2", + "id": "2", + "description": "This is a test issue type", + "iconUrl": "https://jira.example.com/images/icons/issuetypes/newfeature.png", + "name": "Test Issue Type", + "subtask": false + } + ] + }, + "description": { + "required": false, + "schema": { + "type": "string", + "system": "description" + }, + "name": "Description", + "hasDefaultValue": false, + "operations": [ + "set" + ] + }, + "project": { + "required": true, + "schema": { + "type": "project", + "system": "project" + }, + "name": "Project", + "hasDefaultValue": false, + "operations": [ + "set" + ], + "allowedValues": [ + { + "self": "https://jira.example.com/rest/api/2/project/10003", + "id": "10003", + "key": "TEST", + "name": "Test Project", + "projectCategory": { + "self": "https://jira.example.com/rest/api/2/projectCategory/10000", + "id": "10000", + "description": "All Project Catagories", + "name": "All Project" + } + } + ] + }, + "reporter": { + "required": true, + "schema": { + "type": "user", + "system": "reporter" + }, + "name": "Reporter", + "autoCompleteUrl": "https://jira.example.com/rest/api/latest/user/search?username=", + "hasDefaultValue": false, + "operations": [ + "set" + ] + }, + "assignee": { + "required": false, + "schema": { + "type": "user", + "system": "assignee" + }, + "name": "Assignee", + "autoCompleteUrl": "https://jira.example.com/rest/api/latest/user/assignable/search?issueKey=null&username=", + "hasDefaultValue": false, + "operations": [ + "set" + ] + }, + "priority": { + "required": false, + "schema": { + "type": "priority", + "system": "priority" + }, + "name": "Priority", + "hasDefaultValue": true, + "operations": [ + "set" + ], + "allowedValues": [ + { + "self": "https://jira.example.com/rest/api/2/priority/1", + "iconUrl": "https://jira.example.com/images/icons/priorities/blocker.png", + "name": "Blocker", + "id": "1" + }, + { + "self": "https://jira.example.com/rest/api/2/priority/2", + "iconUrl": "https://jira.example.com/images/icons/priorities/critical.png", + "name": "Critical", + "id": "2" + }, + { + "self": "https://jira.example.com/rest/api/2/priority/3", + "iconUrl": "https://jira.example.com/images/icons/priorities/major.png", + "name": "Major", + "id": "3" + }, + { + "self": "https://jira.example.com/rest/api/2/priority/4", + "iconUrl": "https://jira.example.com/images/icons/priorities/minor.png", + "name": "Minor", + "id": "4" + }, + { + "self": "https://jira.example.com/rest/api/2/priority/5", + "iconUrl": "https://jira.example.com/images/icons/priorities/trivial.png", + "name": "Trivial", + "id": "5" + } + ] + }, + "labels": { + "required": false, + "schema": { + "type": "array", + "items": "string", + "system": "labels" + }, + "name": "Labels", + "autoCompleteUrl": "https://jira.example.com/rest/api/1.0/labels/suggest?query=", + "hasDefaultValue": false, + "operations": [ + "add", + "set", + "remove" + ] + } + } +} +'@ + + Mock Get-JiraIssue -ModuleName PSJira { + [PSCustomObject] @{ + ID = $issueID; + Key = $issueKey; + RestUrl = "$jiraServer/rest/api/latest/issue/$issueID"; + } + } + + It "Queries Jira for metadata information about editing an issue" { + { Get-JiraIssueEditMetadata -Issue $issueID } | Should Not Throw + Assert-MockCalled -CommandName Invoke-JiraMethod -ModuleName PSJira -Exactly -Times 1 -Scope It -ParameterFilter {$Method -eq 'Get' -and $URI -like "*/rest/api/*/issue/$issueID/editmeta"} + } + + It "Uses ConvertTo-JiraEditMetaField to output EditMetaField objects if JIRA returns data" { + + # This is a simplified version of what JIRA will give back + Mock Invoke-JiraMethod -ModuleName PSJira { + @{ + fields = [PSCustomObject] @{ + 'a' = 1; + 'b' = 2; + } + } + } + Mock ConvertTo-JiraEditMetaField -ModuleName PSJira {} + + { Get-JiraIssueEditMetadata -Issue $issueID } | Should Not Throw + Assert-MockCalled -CommandName Invoke-JiraMethod -ModuleName PSJira -Exactly -Times 1 -Scope It -ParameterFilter {$Method -eq 'Get' -and $URI -like "*/rest/api/*/issue/$issueID/editmeta"} + + # There are 2 example fields in our mock above, but they should + # be passed to Convert-JiraCreateMetaField as a single object. + # The method should only be called once. + Assert-MockCalled -CommandName ConvertTo-JiraEditMetaField -ModuleName PSJira -Exactly -Times 1 -Scope It + } + } + } +} diff --git a/PSJira/Public/Get-JiraIssueEditMetadata.ps1 b/PSJira/Public/Get-JiraIssueEditMetadata.ps1 new file mode 100644 index 00000000..74d5d53b --- /dev/null +++ b/PSJira/Public/Get-JiraIssueEditMetadata.ps1 @@ -0,0 +1,96 @@ +function Get-JiraIssueEditMetadata +{ + <# + .Synopsis + Returns metadata required to create an issue in JIRA + .DESCRIPTION + This function returns metadata required to create an issue in JIRA - the fields that can be defined in the process of creating an issue. This can be used to identify custom fields in order to pass them to New-JiraIssue. + + This function is particularly useful when your JIRA instance includes custom fields that are marked as mandatory. The required fields can be identified from this See the examples for more details on this approach. + .EXAMPLE + Get-JiraIssueEditMetadata -Project 'TEST' -IssueType 'Bug' + This example returns the fields available when creating an issue of type Bug under project TEST. + .EXAMPLE + Get-JiraIssueEditMetadata -Project 'JIRA' -IssueType 'Bug' | ? {$_.Required -eq $true} + This example returns fields available when creating an issue of type Bug under the project Jira. It then uses Where-Object (aliased by the question mark) to filter only the fields that are required. + .INPUTS + This function does not accept pipeline input. + .OUTPUTS + This function outputs the PSJira.Field objects that represent JIRA's create metadata. + .NOTES + This function requires either the -Credential parameter to be passed or a persistent JIRA session. See New-JiraSession for more details. If neither are supplied, this function will run with anonymous access to JIRA. + #> + [CmdletBinding()] + param( + # Issue id or key + [Parameter(Mandatory = $true, + Position = 0)] + [String] $Issue, + + [String] $ConfigFile, + + # Credentials to use to connect to Jira + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] $Credential + ) + + begin + { + Write-Debug "[Get-JiraIssueEditMetadata] Reading server from config file" + try + { + $server = Get-JiraConfigServer -ConfigFile $ConfigFile -ErrorAction Stop + } catch { + $err = $_ + Write-Debug "[Get-JiraIssueEditMetadata] Encountered an error reading the Jira server." + throw $err + } + + Write-Debug "[Get-JiraIssueEditMetadata] Building URI for REST call based on parameters" + $uri = "$server/rest/api/latest/issue/$Issue/editmeta" + } + + process + { + Write-Debug "[Get-JiraIssueEditMetadata] Preparing for blastoff!" + $jiraResult = Invoke-JiraMethod -Method Get -URI $uri -Credential $Credential + + if ($jiraResult) + { + if (@($jiraResult.projects).Count -eq 0) + { + Write-Debug "[Get-JiraIssueEditMetadata] No project results were found. Throwing exception." + throw "No projects were found for the given project [$Project]. Use Get-JiraProject for more details." + } elseif (@($jiraResult.projects).Count -gt 1) { + Write-Debug "[Get-JiraIssueEditMetadata] Multiple project results were found. Throwing exception." + throw "Multiple projects were found for the given project [$Project]. Refine the parameters to return only one project." + } + + $projectId = $jiraResult.projects.id + $projectKey = $jiraResult.projects.key + + Write-Debug "[Get-JiraIssueEditMetadata] Identified project key: [$Project]" + + if (@($jiraResult.projects.issuetypes) -eq 0) + { + Write-Debug "[Get-JiraIssueEditMetadata] No issue type results were found. Throwing exception." + throw "No issue types were found for the given issue type [$IssueType]. Use Get-JiraIssueType for more details." + } elseif (@($jiraResult.projects.issuetypes).Count -gt 1) { + Write-Debug "[Get-JiraIssueEditMetadata] Multiple issue type results were found. Throwing exception." + throw "Multiple issue types were found for the given issue type [$IssueType]. Refine the parameters to return only one issue type." + } + + Write-Debug "[Get-JiraIssueEditMetadata] Converting results to custom object" + $obj = ConvertTo-JiraEditMetaField -InputObject $jiraResult + + Write-Debug "Outputting results" + Write-Output $obj + +# Write-Output $jiraResult + } else { + Write-Debug "[Get-JiraIssueEditMetadata] No results were returned from JIRA." + } + } +} + +