From 13f28c181b0723de0afb1bdf406de56e4b1ede19 Mon Sep 17 00:00:00 2001 From: Oliver Lipkau Date: Tue, 8 Nov 2016 13:26:32 +0100 Subject: [PATCH 1/5] added Get-JiraIssueEditMetadata --- .../Internal/ConvertTo-JiraEditMeraField.ps1 | 99 +++++++++++++++++++ PSJira/Public/Get-JiraIssueEditMetadata.ps1 | 96 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 PSJira/Internal/ConvertTo-JiraEditMeraField.ps1 create mode 100644 PSJira/Public/Get-JiraIssueEditMetadata.ps1 diff --git a/PSJira/Internal/ConvertTo-JiraEditMeraField.ps1 b/PSJira/Internal/ConvertTo-JiraEditMeraField.ps1 new file mode 100644 index 00000000..958a2ef0 --- /dev/null +++ b/PSJira/Internal/ConvertTo-JiraEditMeraField.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/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." + } + } +} + + From 8866c90309c5c2bb0ee2cd3ff45d932ee10e2782 Mon Sep 17 00:00:00 2001 From: Oliver Lipkau Date: Tue, 8 Nov 2016 13:26:45 +0100 Subject: [PATCH 2/5] updated manifest to export function --- PSJira/PSJira.psd1 | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 From f01aed72e3669374072d66b7e13743f0bfe9aa67 Mon Sep 17 00:00:00 2001 From: Oliver Lipkau Date: Tue, 8 Nov 2016 13:26:59 +0100 Subject: [PATCH 3/5] added Unit Tests --- .../ConvertTo-JiraEditMetaField.Tests.ps1 | 113 ++++++++ .../Public/Get-JiraIssueEditMetadata.Test.ps1 | 252 ++++++++++++++++++ 2 files changed, 365 insertions(+) create mode 100644 PSJira/Internal/ConvertTo-JiraEditMetaField.Tests.ps1 create mode 100644 PSJira/Public/Get-JiraIssueEditMetadata.Test.ps1 diff --git a/PSJira/Internal/ConvertTo-JiraEditMetaField.Tests.ps1 b/PSJira/Internal/ConvertTo-JiraEditMetaField.Tests.ps1 new file mode 100644 index 00000000..b225717c --- /dev/null +++ b/PSJira/Internal/ConvertTo-JiraEditMetaField.Tests.ps1 @@ -0,0 +1,113 @@ +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".") +. "$here\$sut" + +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/Public/Get-JiraIssueEditMetadata.Test.ps1 b/PSJira/Public/Get-JiraIssueEditMetadata.Test.ps1 new file mode 100644 index 00000000..4b3fe270 --- /dev/null +++ b/PSJira/Public/Get-JiraIssueEditMetadata.Test.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-WebRequest" -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/createmeta?projectIds=10003&issuetypeIds=2&expand=projects.issuetypes.fields'} + } + + It "Uses ConvertTo-JiraCreateMetaField 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-JiraCreateMetaField -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/createmeta?projectIds=10003&issuetypeIds=2&expand=projects.issuetypes.fields'} + + # 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 + } + } + } +} From c2f307a0121d9be23426f06e1aeeda37ba98d884 Mon Sep 17 00:00:00 2001 From: Oliver Lipkau Date: Wed, 9 Nov 2016 14:24:05 +0100 Subject: [PATCH 4/5] Fixed naming of Unit Test file --- ...eEditMetadata.Test.ps1 => Get-JiraIssueEditMetadata.Tests.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename PSJira/Public/{Get-JiraIssueEditMetadata.Test.ps1 => Get-JiraIssueEditMetadata.Tests.ps1} (100%) diff --git a/PSJira/Public/Get-JiraIssueEditMetadata.Test.ps1 b/PSJira/Public/Get-JiraIssueEditMetadata.Tests.ps1 similarity index 100% rename from PSJira/Public/Get-JiraIssueEditMetadata.Test.ps1 rename to PSJira/Public/Get-JiraIssueEditMetadata.Tests.ps1 From 1f361bffb4c2f2fb4af5df0bd661adac74c7f0c0 Mon Sep 17 00:00:00 2001 From: Oliver Lipkau Date: Wed, 9 Nov 2016 15:11:11 +0100 Subject: [PATCH 5/5] fixed name of Function file --- ...rtTo-JiraEditMeraField.ps1 => ConvertTo-JiraEditMetaField.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename PSJira/Internal/{ConvertTo-JiraEditMeraField.ps1 => ConvertTo-JiraEditMetaField.ps1} (100%) diff --git a/PSJira/Internal/ConvertTo-JiraEditMeraField.ps1 b/PSJira/Internal/ConvertTo-JiraEditMetaField.ps1 similarity index 100% rename from PSJira/Internal/ConvertTo-JiraEditMeraField.ps1 rename to PSJira/Internal/ConvertTo-JiraEditMetaField.ps1