diff --git a/.docs/Add-VSTeamWorkItemRelation.md b/.docs/Add-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..8cd5a25be --- /dev/null +++ b/.docs/Add-VSTeamWorkItemRelation.md @@ -0,0 +1,87 @@ + + +# Add-VSTeam + +## SYNOPSIS + + + +## SYNTAX + +## DESCRIPTION + + + +## EXAMPLES + +### Example 1: Adds workitems as children + +```powershell +50, 51 | Add-VSTeamWorkItemRelation -RelationType Child -OfRelatedId 80 +``` +This command adds 50 and 51 as child of 80 + +### Example 2: Adds workitems as children + +```powershell +Add-VSTeamWorkItemRelation Id 50,51 -RelationType Child -OfRelatedId 80 +``` +This command adds 50 and 51 as child of 80 + +## PARAMETERS + +### Id + +IDs of the elements whose relate with RelatedId + +```yaml +Type: int[] +Required: True +Position: 0 +Accept pipeline input: true (ByPropertyName, ByValue) +``` + +### RelationType + +Specify the relation type. The relation name is translated to the technical name. +You can tab complete from a list of available relation types. Also you can get a list of relation types using the Get-VSTeamWorkItemRelationType CmdLet + +```yaml +Type: String +Required: True +Position: 1 +``` + +### OfRelatedId + +Id of the related work item +All the work items identified by the IDs will be related with this work item. For example, you cannot specify a -RelationType Parent because the +cmdlet will try to assing all the IDs as parent of the related work item + +```yaml +Type: int +Required: True +Position: 2 +``` + + + +## INPUTS + +## OUTPUTS + +### System.String + +Returns the work item identified by OfRelatedId + +## NOTES + +This cmdlet is a shorcut for New-VSTeamWorkItemRelation and Update-VSTeamWorkItem. + + + +## RELATED LINKS + +[Update-VSTeamWorkItem](Update-VSTeamWorkItem.md) +[New-VSTeamWorkItemRelation](New-VSTeamWorkItemRelation.md) +[Switch-VSTeamWorkItemParent](Switch-VSTeamWorkItemParent.md) diff --git a/.docs/Get-VSTeamWorkItemRelation.md b/.docs/Get-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..95ff07235 --- /dev/null +++ b/.docs/Get-VSTeamWorkItemRelation.md @@ -0,0 +1,51 @@ + + +# Get-VSTeamWorkItemRelation + +## SYNOPSIS + + + +## SYNTAX + +## Description + + + +## EXAMPLES + +### Example 1 + +```powershell +Get-VSTeamWorkItemRelation -Id 55 +``` + +This command retrieves a list of relations from a single work item. + +## PARAMETERS + +### Id +Work item ID + +```yaml +Type: int +``` + + +## INPUTS + +### None + +## OUTPUTS + +### System.Object + + + + +## RELATED LINKS + +[New-VSTeamWorkItemRelation](New-VSTeamWorkItemRelation.md) +[Switch-VSTeamWorkItemParent](Switch-VSTeamWorkItemParent.md) +[Update-VSTeamWorkItem](Update-VSTeamWorkItem.md) +[Remove-VSTeamWorkItemRelation](Remove-VSTeamWorkItemRelation.md) \ No newline at end of file diff --git a/.docs/Get-VSTeamWorkItemRelationType.md b/.docs/Get-VSTeamWorkItemRelationType.md new file mode 100644 index 000000000..c50294b3d --- /dev/null +++ b/.docs/Get-VSTeamWorkItemRelationType.md @@ -0,0 +1,49 @@ + + +# Get-VSTeamWorkItemType + +## SYNOPSIS + + + +## SYNTAX + +## Description + + + +## EXAMPLES + +### Example 1 + +```powershell +Get-VSTeamWorkItemRelationType -Usage WorkItemLink +``` + +This command gets a single work item type. + +## PARAMETERS + +### Usage +Usage of the relations. The acceptable values for this parameter are (Default value is WorkItemLink): +- All +- ResourceLink +- WorkItemLink + +```yaml +Type: String +``` + + +## INPUTS + +### None + +## OUTPUTS + +### System.Object + + + + +## RELATED LINKS diff --git a/.docs/New-VSTeamWorkItemRelation.md b/.docs/New-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..e0cf543b5 --- /dev/null +++ b/.docs/New-VSTeamWorkItemRelation.md @@ -0,0 +1,174 @@ + + +# New-VSTeamWorkItemRelation + +## SYNOPSIS + + + +## SYNTAX + +## DESCRIPTION + + + +## EXAMPLES + +### Example 1 + +```powershell +New-VSTeamWorkItemRelation -RelationType Duplicate -Id 55 -Comment "My comment" + +ID RelationType Operation Index Comment +-- ------------ --------- ----- ------- +55 System.LinkTypes.Hierarchy-Reverse add - My comment +``` +Simple invocation, returns a Relation object. + + +### Example 2 + +```powershell +$relations = New-VSTeamWorkItemRelation -Operation Remove -Index 0 | + New-VSTeamWorkItemRelation -Operation Remove -Index 1 +Update-VSTeamWorkItem -Id 30 -Relations $relations +``` +Removes work first 2 links from work item 30 + +### Example 3 + +```powershell +$relations =@() +$relations += New-VSTeamWorkItemRelation -Operation Remove -Index 0 +$relations += New-VSTeamWorkItemRelation -RelationType Related -Id 66 +Update-VSTeamWorkItem -Id 30 -Relations $relations +``` +Similar to example 2, but managing the relations collection directly +Pay attention that the first relation, for Operation Remove, only the Index parameter is provided. +In the second relation, when provided the Id, it's necessary to provide the RealationType. The operation will be always Add. + +### Example 4 + +```powershell +$relations = 55,66 | New-VSTeamWorkItemRelation -RelationType Child +Update-VSTeamWorkItem -Id 30 -Relations $relations + +``` +Adds work items 55 and 56 as children of 30 +Pay attention that this use case, passing a list of IDs from pipeline, has sense only for 'Add' operation, and because this is the defaut operation value, we can ommit the Operation parameter + +### Example 5 + +```powershell +$relations = Get-VSTeamWorkItem -Id 55 | New-VSTeamWorkItemRelation -RelationType Duplicate -Comment "is it dupllicate?" +Update-VSTeamWorkItem -Id 30 -Relations $relations + +``` +Adds work items 55 as duplicate of 30 +Pay attention that this use case, passing a list of work items from pipeline, has sense only for 'Add' operation, and because this is the defaut operation value and the Operation parameter is not allowed when you provide the Id + +### Example 6 + +```powershell +$relations = Get-VSTeamWiql -Id "f87b028b-0528-47d6-b517-2d82af680295" | + Select-Object -ExpandProperty WorkItems | + New-VSTeamWorkItemRelation -RelationType Related +Update-VSTeamWorkItem -Id 30 -Relations $relations +``` +Adds all work items returned by a query as related to work item 30 +Pay attention that this use case, passing a list of work items from pipeline, has sense only for 'Add' operation, and because this is the defaut operation value, we can ommit the Operation parameter + +### Example 7 +```powershell +$relation = New-VSTeamWorkItemRelation -RelationType Related -Operation Replace -Comment "updated comment" +Update-VSTeamWorkItem -Id 30 -Relations $relations +``` +Updates the comment of a relation. The replace operation only supports comment update. +If you really need to change a relation, like re-parent a work item, you need to create two relations: first, remove and then add operations. + +### Example 8 +```powershell +$relations =@() +$id = Get-VSTeamWorkItem -id 30 -Expand Relation +for ($i=0; $i -lt $id.relations.Count; $i++) { + $relations += New-VSTeamWorkItemRelation -Operation Remove -Index $i +} +Update-VSTeamWorkItem -Id 30 -Relations $relations +``` +Removes all the links from work item 30 + +## PARAMETERS + +### InputObject + +Operation: Intended for fluent syntax (see Example 2) + +```yaml +Type: PSCustomObject[] +Parameter Sets: ByObject +Required: True +Accept pipeline input: true +``` + +### Id + +Related WorkItem id + +This is not the work item to be updated, but the work item that will be part of the relations of the updated work item +Can be used as parameter or pass a list of ID's or a list of work items from the pipeline + +```yaml +Type: int[] +Parameter Sets: ByID,ByObject +Required: True +Accept pipeline input: true +``` + +### RelationType + +Specify the relation type. The relation name is translated to the technical name. +You can tab complete from a list of available relation types. Also you can get a list of relation types using the Get-VSTeamWorkItemRelationType CmdLet +Not allowed when you specify an index in the remove or replace operations + +```yaml +Type: String +Parameter Sets: ByID,ByRelation +Required: True (in ByID parameterset) +``` + +### Operation + +Remove or Replace a relation +The Add operation is implicit when the Id parameter is used. So this parameter is only valid when Index is specified + +```yaml +Type: string +Parameter Sets: ByIndex,ByRelation +Required: False +Accepted values: Remove, Replace +``` + +### Comment + +Add (or edit -with Replace operation-) a comment to the relation + +```yaml +Type: string +Required: False +``` + +## OUTPUTS + +### vsteam_lib.WorkItemRelation + +## NOTES + +This CmdLet do not modify any work item, just generates a JSON Patch compatible object that describes the updates to be applied using the Update-VSTeamWorkItem + + + +## RELATED LINKS + +[Get-VSTeamWorkItemRelationType](Get-VSTeamWorkItemRelationType.md) +[Get-VSTeamWorkItem](Get-VSTeamWorkItem.md) +[Update-VSTeamWorkItem](Update-VSTeamWorkItem.md) \ No newline at end of file diff --git a/.docs/Remove-VSTeamWorkItemRelation.md b/.docs/Remove-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..2da340c23 --- /dev/null +++ b/.docs/Remove-VSTeamWorkItemRelation.md @@ -0,0 +1,65 @@ + + +# Remove-VSTeamWorkItemTag + +## SYNOPSIS + + + +## SYNTAX + +## DESCRIPTION + +This cmdlet is a shortcut of Get-VSTeamWorkItem -Id $Id -Expand Relations, and Update-VSTeamWorkItem -Id $Id -Relations $relationsToRemove +Allows easily to remove the relation of 2 workitems just knowing the IDs + +## EXAMPLES + +### Example 1 + +```powershell +55, 66 | Remove-VSTeamWorkItemRelation -FromRelatedId 25 +``` +Imagine 55 and 66 work items are children of 25. This command will remove the Child relationship from 55 to 25 and from 66 to 25 + +### Example 2 + +```powershell +Remove-VSTeamWorkItemRelation -Id 25 -FromRelatedId 55, 66 +``` +Imagine 55 and 66 work items are children of 25. This command will remove the Parent relationship from 25 to 55 and 66 + + +## PARAMETERS + + + +### Id + +Id of the work item + +```yaml +Type: Int32[] +Required: True +Accept pipeline input: true (ByPropertyName, ByValue) + +``` +### FromRelatedId +Id of the related work item +Type: Int32[] +Required: True + +## INPUTS + +## OUTPUTS + +## NOTES + + + +## RELATED LINKS + +[Get-VSTeamWorkItem](Get-VSTeamWorkItem.md) +[Get-VSTeamWorkItemRelation](Get-VSTeamWorkItemRelation.md) +[New-VSTeamWorkItemRelation](New-VSTeamWorkItemRelation.md) +[Update-VSTeamWorkItem](Update-VSTeamWorkItem.md) diff --git a/.docs/Switch-VSTeamWorkItemParent.md b/.docs/Switch-VSTeamWorkItemParent.md new file mode 100644 index 000000000..84fa17acf --- /dev/null +++ b/.docs/Switch-VSTeamWorkItemParent.md @@ -0,0 +1,80 @@ + + +# Add-VSTeam + +## SYNOPSIS + + + +## SYNTAX + +## DESCRIPTION + + + +## EXAMPLES + +### Example 1: Replace the paretn in 2 work items + +```powershell +50, 51 | Switch-VSTeamWorkItemParent -ParentId 80 +``` +This command replaces the parent in workitems with ID 50 and 51 and assign work item 80 as parent +If any of the IDs provided don't have a parent, they shall remain they are. + +### Example 2: Replace the paretn in 2 work items + +```powershell +Switch-VSTeamWorkItemParent Id 50,51 -ParentId 80 -AddParent +``` +This command replaces the parent in workitems with ID 50 and 51 and assign work item 80 as parent +If any of the IDs provided don't have a parent, the work item 80 is assigned + +## PARAMETERS + +### Id + +IDs of the elements whose parent is to be replaced + +```yaml +Type: int[] +Required: True +Position: 0 +Accept pipeline input: true (ByPropertyName, ByValue) +``` + +### ParentId + +Id of the new parent work item + +```yaml +Type: int +Required: True +Position: 1 +``` + +### AddParent + +If present, if the work item currently doen't have a parent, it will assign $ParentId as parent +If not present, if the work item currently doen't have a parent, it shall remain as is. + +```yaml +Type: switch +Required: False +Position: 3 +``` + + + +## INPUTS + +## OUTPUTS + +## NOTES + + + +## RELATED LINKS + +[Update-VSTeamWorkItem](Update-VSTeamWorkItem.md) +[New-VSTeamWorkItemRelation](New-VSTeamWorkItemRelation.md) diff --git a/.docs/Update-VSTeamWorkItem.md b/.docs/Update-VSTeamWorkItem.md index af75c75d1..f5a4a41c0 100644 --- a/.docs/Update-VSTeamWorkItem.md +++ b/.docs/Update-VSTeamWorkItem.md @@ -106,6 +106,14 @@ Type: Hashtable Required: False ``` +### Relations +Array of vsteam_lib.WorkItemRelation that describes the relations to be added, removed or updated in/from the work item + +```yaml +Type: PSCustomObject[] +Required: False +``` + ## INPUTS @@ -125,3 +133,5 @@ Any of the basic work item parameters defined in this method, will cause an exce ## RELATED LINKS + +[New-VSTeamWorkItemRelation](New-VSTeamWorkItemRelation.md) \ No newline at end of file diff --git a/.docs/synopsis/Add-VSTeamWorkItemRelation.md b/.docs/synopsis/Add-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..43f9e1ca3 --- /dev/null +++ b/.docs/synopsis/Add-VSTeamWorkItemRelation.md @@ -0,0 +1 @@ +Adds a relationship between different workitems \ No newline at end of file diff --git a/.docs/synopsis/Get-VSTeamWorkItemRelation.md b/.docs/synopsis/Get-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..f0ff64d70 --- /dev/null +++ b/.docs/synopsis/Get-VSTeamWorkItemRelation.md @@ -0,0 +1,2 @@ +Retrieves a list of relations from a single work item. +This cmdlet is a shortcut for `Get-VSTeamWorkItem -Id $Id -Expand Relations | Select-Object -ExpandProperty Relations` and returns a list of relations compatible with the *-VSTeamWorkItemRelation and Switch-VSTeamWorkItemParent cmdlets diff --git a/.docs/synopsis/Get-VSTeamWorkItemRelationType.md b/.docs/synopsis/Get-VSTeamWorkItemRelationType.md new file mode 100644 index 000000000..b998c8047 --- /dev/null +++ b/.docs/synopsis/Get-VSTeamWorkItemRelationType.md @@ -0,0 +1 @@ +Returns a list of the different relation types between work items and links inside the same work item \ No newline at end of file diff --git a/.docs/synopsis/New-VSTeamWorkItemRelation.md b/.docs/synopsis/New-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..e73363816 --- /dev/null +++ b/.docs/synopsis/New-VSTeamWorkItemRelation.md @@ -0,0 +1 @@ +Helper cmdlet that creates an in-memory Relation object to facilitate building the -Relations parameter in Update-VSTeamWorkItem \ No newline at end of file diff --git a/.docs/synopsis/Remove-VSTeamWorkItemRelation.md b/.docs/synopsis/Remove-VSTeamWorkItemRelation.md new file mode 100644 index 000000000..7fd3a1c92 --- /dev/null +++ b/.docs/synopsis/Remove-VSTeamWorkItemRelation.md @@ -0,0 +1 @@ +Removes the relation from one or more workitems and one or more related workitems \ No newline at end of file diff --git a/.docs/synopsis/Switch-VSTeamWorkItemParent.md b/.docs/synopsis/Switch-VSTeamWorkItemParent.md new file mode 100644 index 000000000..5ad6537b3 --- /dev/null +++ b/.docs/synopsis/Switch-VSTeamWorkItemParent.md @@ -0,0 +1,3 @@ +Replaces the parent of one or more work items +It also allows to add a parent if the work item currently doesn't have one +If current parent Id is equal to the new one, no action is done \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index eab9763c8..c59d3cc7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ # Changelog ## 7.14.0 + Merged [Pull Request #545](https://github.com/MethodsAndPractices/vsteam/pull/545) from [Sebastian Schütze](https://github.com/SebastianSchuetze) the following: - Added new cmdlets for banner management: `Add-VSTeamBanner`, `Update-VSTeamBanner`, `Get-VSTeamBanner` and `Remove-VSTeamBanner`. @@ -14,7 +15,6 @@ Merged [Pull Request](https://github.com/MethodsAndPractices/vsteam/pull/539) fr Merged [Pull Request](https://github.com/MethodsAndPractices/vsteam/pull/536) from [Sebastian Schütze](https://github.com/SebastianSchuetze) the following: - Fix missing switch OverwriteMask to all cmdlets who are dependant to Add-VSTeamAccessControlList - ## 7.13.1 Merged [Pull Request](https://github.com/MethodsAndPractices/vsteam/pull/532) from [Miguel Nieto](https://github.com/mnieto) the following: - Fix Set-VSTeamAccount Error on Module Import [531](https://github.com/MethodsAndPractices/vsteam/issues/531) diff --git a/Source/Classes/Attribute/RelationTypeToReferenceNameAttribute.cs b/Source/Classes/Attribute/RelationTypeToReferenceNameAttribute.cs new file mode 100644 index 000000000..59f30346d --- /dev/null +++ b/Source/Classes/Attribute/RelationTypeToReferenceNameAttribute.cs @@ -0,0 +1,19 @@ +using System.Management.Automation; + +namespace vsteam_lib +{ + public sealed class RelationTypeToReferenceNameAttribute : ArgumentTransformationAttribute + { + public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) + { + var value = inputData?.ToString(); + + if (!string.IsNullOrEmpty(value)) + { + value = RelationTypeCache.GetReferenceName(value); + } + + return value; + } + } +} diff --git a/Source/Classes/Cache/RelationTypeCache.cs b/Source/Classes/Cache/RelationTypeCache.cs new file mode 100644 index 000000000..6f0d47ddf --- /dev/null +++ b/Source/Classes/Cache/RelationTypeCache.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; + +namespace vsteam_lib +{ + public static class RelationTypeCache + { + private const string commandName = "Get-VSTeamWorkItemRelationType"; + internal static InternalCache Cache { get; } = new InternalCache(commandName, "Name", false); + + internal static bool HasCacheExpired => Cache.HasCacheExpired; + internal static Dictionary ReferenceNames { get; } = new Dictionary(); + + public static void Invalidate() => Cache.Invalidate(); + + public static void Update(Dictionary cacheItems) + { + ReferenceNames.Clear(); + IEnumerable list; + + // If a list is passed in use it. If not call Get-VSTeamWorkItemRelationType + if (null == cacheItems) + { + Cache.Shell.Commands.Clear(); + + // Use this to store the relationNames + var relations = Cache.Shell.AddCommand(commandName) + .AddParameter("Usage", "WorkItemLink") + .Invoke(); + + foreach (var relation in relations) + { + ReferenceNames.Add(relation.Properties["Name"].Value.ToString(), relation.Properties["ReferenceName"].Value.ToString()); + } + + // This will return just the names + list = Cache.Shell.AddCommand("Select-Object") + .AddParameter("ExpandProperty", "Name") + .AddCommand("Sort-Object") + .Invoke(relations); + + } + else + { + foreach(var relation in cacheItems) { + ReferenceNames.Add(relation.Key, relation.Value); + } + list = cacheItems.Keys; + } + + Cache.PreFill(list); + } + + public static IEnumerable GetCurrent() + { + if (HasCacheExpired) + { + Update(null); + } + + return Cache.Values; + } + + public static string GetReferenceName(string name) + { + if (ReferenceNames.Keys.Count == 0) { + Update(null); + } + return ReferenceNames[name]; + } + } +} diff --git a/Source/Classes/Completer/WorkItemRelationTypeCompleter.cs b/Source/Classes/Completer/WorkItemRelationTypeCompleter.cs new file mode 100644 index 000000000..856ff520c --- /dev/null +++ b/Source/Classes/Completer/WorkItemRelationTypeCompleter.cs @@ -0,0 +1,37 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Management.Automation; +using System.Management.Automation.Abstractions; +using System.Management.Automation.Language; + +namespace vsteam_lib +{ + public class WorkItemRelationTypeCompleter : BaseCompleter + { + /// + /// This constructor is used when running in a PowerShell session. It cannot be + /// loaded in a unit test. + /// + [ExcludeFromCodeCoverage] + public WorkItemRelationTypeCompleter() : base() { } + + /// + /// This constructor is used during unit testings + /// + /// fake instance of IPowerShell used for testing + public WorkItemRelationTypeCompleter(IPowerShell powerShell) : base(powerShell) { } + + public override IEnumerable CompleteArgument(string commandName, + string parameterName, + string wordToComplete, + CommandAst commandAst, + IDictionary fakeBoundParameters) + { + var values = new List(); + + SelectValues(wordToComplete, RelationTypeCache.GetCurrent(), values); + return values; + } + } +} \ No newline at end of file diff --git a/Source/Private/applyTypes.ps1 b/Source/Private/applyTypes.ps1 index 96f30cbd5..551bcd7b9 100644 --- a/Source/Private/applyTypes.ps1 +++ b/Source/Private/applyTypes.ps1 @@ -187,4 +187,18 @@ function _applyTypesToAgentPoolMaintenance { [CmdletBinding()] param($item) $item.PSObject.TypeNames.Insert(0, 'vsteam_lib.AgentPoolMaintenance') +} + +function _applyTypesToWorkItemRelationType { + [CmdletBinding()] + param ($item) + + $item.PSObject.TypeNames.Insert(0, 'vsteam_lib.WorkItemRelationType') +} + +function _applyTypesToWorkItemRelation { + [CmdletBinding()] + param ($item) + + $item.PSObject.TypeNames.Insert(0, 'vsteam_lib.WorkItemRelation') } \ No newline at end of file diff --git a/Source/Public/Add-VSTeamWorkItemRelation.ps1 b/Source/Public/Add-VSTeamWorkItemRelation.ps1 new file mode 100644 index 000000000..4a4c98593 --- /dev/null +++ b/Source/Public/Add-VSTeamWorkItemRelation.ps1 @@ -0,0 +1,20 @@ +function Add-VSTeamWorkItemRelation { + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Medium", HelpUri = 'https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/Add-VSTeamWorkItemRelation')] + param ( + [Parameter(Mandatory, Position=0, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [int[]]$Id, + [ArgumentCompleter([vsteam_lib.WorkItemRelationTypeCompleter])] + [Parameter(Mandatory, Position=1)] + [string]$RelationType, + [Parameter(Mandatory, Position=2)] + [int]$OfRelatedId, + [switch]$Force + ) + + process { + $relations = $Id | New-VSTeamWorkItemRelation -RelationType $RelationType + if ($Force -or $pscmdlet.ShouldProcess($Id, "add relations to work items")) { + Update-VSTeamWorkItem -Id $OfRelatedId -Relations $relations + } + } +} \ No newline at end of file diff --git a/Source/Public/Get-VSTeamWorkItemRelation.ps1 b/Source/Public/Get-VSTeamWorkItemRelation.ps1 new file mode 100644 index 000000000..9dc378901 --- /dev/null +++ b/Source/Public/Get-VSTeamWorkItemRelation.ps1 @@ -0,0 +1,16 @@ +function Get-VSTeamWorkItemRelation { + [CmdletBinding(HelpUri='https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/Get-VSTeamWorkItemRelation')] + param ( + [int]$Id + ) + + $relations = Get-VSTeamWorkItem -Id $Id -Expand Relations | Select-Object -ExpandProperty Relations + $i = 0 + foreach ($relation in $relations) { + $relatedId = $relation.Url.split("/")[-1] + $r = New-VSTeamWorkItemRelation -Id $relatedId -RelationType $relation.Attributes.Name -Comment $relation.Attributes.Comment + $r.Index = $i++ + $r + } + +} \ No newline at end of file diff --git a/Source/Public/Get-VSTeamWorkItemRelationType.ps1 b/Source/Public/Get-VSTeamWorkItemRelationType.ps1 new file mode 100644 index 000000000..17db0e6fe --- /dev/null +++ b/Source/Public/Get-VSTeamWorkItemRelationType.ps1 @@ -0,0 +1,25 @@ +function Get-VSTeamWorkItemRelationType { + [CmdletBinding(HelpUri='https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/Get-VSTeamWorkItemRelationType')] + param ( + [ValidateSet('All', 'ResourceLink', 'WorkItemLink')] + [string]$Usage = 'WorkItemLink' + ) + + process { + $commonArgs = @{ + area = 'wit' + resource = 'workitemrelationtypes' + version = $(_getApiVersion Core) + noProject = $true + } + $resp = _callAPI @commonArgs + + foreach ($item in $resp.value) { + if ($Usage -eq 'All' -or $Usage -eq $item.attributes.usage) { + _applyTypesToWorkItemRelationType -item $item + Write-Output $item + } + } + } + +} \ No newline at end of file diff --git a/Source/Public/New-VSTeamWorkItemRelation.ps1 b/Source/Public/New-VSTeamWorkItemRelation.ps1 new file mode 100644 index 000000000..18450ba33 --- /dev/null +++ b/Source/Public/New-VSTeamWorkItemRelation.ps1 @@ -0,0 +1,88 @@ +function New-VSTeamWorkItemRelation { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Scope="Function", Justification="No calls against the Azure DevOps API are done. No persistent changes.")] + [CmdletBinding(DefaultParameterSetName="ById", HelpUri="https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/New-VSTeamWorkItemRelation")] + param( + [Parameter(Mandatory, ValueFromPipeline, ParameterSetName="ByObject")] + [PSCustomObject[]]$InputObject, + + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName="ById")] + [Parameter(ParameterSetName="ByObject")] + [int[]]$Id, + + [ArgumentCompleter([vsteam_lib.WorkItemRelationTypeCompleter])] + [vsteam_lib.RelationTypeToReferenceNameAttribute()] + [Parameter(Mandatory, ParameterSetName="ById")] + [Parameter(ParameterSetName="ByObject")] + [string]$RelationType, + + [ValidateSet('Remove', 'Replace')] + [Parameter(Mandatory, ParameterSetName="ByIndex")] + [Parameter(ParameterSetName="ByObject")] + [string]$Operation, + + [Parameter(Mandatory, ParameterSetName="ByIndex")] + [Parameter(ParameterSetName="ByObject")] + [int]$Index, + + [string]$Comment + ) + + process { + + $results = @() + + if ($PSCmdlet.ParameterSetName -eq "ByObject") { + + if ($null -eq $PSBoundParameters.Index) { + + $results += [PSCustomObject]@{ + Id = $Id[0] + RelationType = $RelationType + Operation = "add" + Comment = $Comment + Index = "-" + } + + } else { + + $results += [PSCustomObject]@{ + Id = $null + RelationType = $null + Operation = $Operation.ToLower() + Comment = $Comment + Index = $PSBoundParameters.Index + } + } + + } elseif ($PSCmdlet.ParameterSetName -eq "ById") { + + foreach ($item in $Id) { + + $results += [PSCustomObject]@{ + Id = $item + RelationType = $RelationType + Operation = 'add' + Comment = $Comment + Index = '-' + } + + } + + } else { + + $results += [PSCustomObject]@{ + Id = $null + RelationType = $RelationType + Operation = $Operation.ToLower() + Comment = $Comment + Index = $index + } + + } + + foreach ($item in $results){ + _applyTypesToWorkItemRelation $item + return $item + } + } +} \ No newline at end of file diff --git a/Source/Public/Remove-VSTeamWorkItemRelation.ps1 b/Source/Public/Remove-VSTeamWorkItemRelation.ps1 new file mode 100644 index 000000000..bb3bb3164 --- /dev/null +++ b/Source/Public/Remove-VSTeamWorkItemRelation.ps1 @@ -0,0 +1,26 @@ +function Remove-VSTeamWorkItemRelation { + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Medium", HelpUri = 'https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/Remove-VSTeamWorkItemRelation')] + param ( + [Parameter(Mandatory, Position=0, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [int[]]$Id, + [Parameter(Mandatory, Position=2)] + [int[]]$FromRelatedId, + [switch]$Force + ) + + process { + foreach($item in $Id) { + $relations = Get-VSTeamWorkItemRelation -Id $item + $relationsToRemove = @() + foreach($relatedId in $FromRelatedId) { + $relation = $relations | Where-Object Id -eq $relatedId + if ($relation) { + $relationsToRemove += New-VSTeamWorkItemRelation -Index $relation.Index -Operation Remove + } + } + if ($Force -or $pscmdlet.ShouldProcess($Id, "remove relations from work items")) { + Update-VSTeamWorkItem -Id $item -Relations $relationsToRemove + } + } + } +} \ No newline at end of file diff --git a/Source/Public/Switch-VSTeamWorkItemParent.ps1 b/Source/Public/Switch-VSTeamWorkItemParent.ps1 new file mode 100644 index 000000000..02b46efa4 --- /dev/null +++ b/Source/Public/Switch-VSTeamWorkItemParent.ps1 @@ -0,0 +1,64 @@ +function Switch-VSTeamWorkItemParent { + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Medium", HelpUri = 'https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/Switch-VSTeamWorkItemParent')] + param ( + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [int[]]$Id, + [Parameter(Mandatory)] + [int]$ParentId, + [switch]$AddParent, + [switch]$Force + ) + + process { + + $workItem = $null + + foreach($item in $Id) { + $ParentNeeded = $false + $workItem = Get-VSTeamWorkItem -Id $item -Expand Relations + + if ($null -eq $workItem.Relations -and $AddParent) { + $ParentNeeded = $true + } + elseif ($workItem.Relations) { + + $relations = $workItem | Select-Object -ExpandProperty Relations + $index = 0 + $hasParent = $false + + foreach($relation in $relations) { + + if ($relation.rel -eq 'System.LinkTypes.Hierarchy-Reverse') { + $hasParent = $true + if ($relation.url.split('/')[-1] -ne $ParentId) { + + $patch = New-VSTeamWorkItemRelation -Index $index -Operation Remove | New-VSTeamWorkItemRelation -Id $ParentId -RelationType Parent + + if ($Force -or $pscmdlet.ShouldProcess($item, "Update-WorkItem")) { + $workItem = Update-VSTeamWorkItem -Id $item -Relations $patch + } + + break + } + } + $index++ + + } + + if (-not $hasParent -and $AddParent) { + $ParentNeeded = $true + } + } + + + if ($ParentNeeded -and ($Foce -or $pscmdlet.ShouldProcess($Id, "Updates the parent work item"))) { + + $newRelation = New-VSTeamWorkItemRelation -Id $ParentId -RelationType Parent + $workItem = Update-VSTeamWorkItem -Id $item -Relations $newRelation + + } + + return $workItem + } + } +} \ No newline at end of file diff --git a/Source/Public/Update-VSTeamWorkItem.ps1 b/Source/Public/Update-VSTeamWorkItem.ps1 index b5d642bb5..2c8a78018 100644 --- a/Source/Public/Update-VSTeamWorkItem.ps1 +++ b/Source/Public/Update-VSTeamWorkItem.ps1 @@ -17,6 +17,9 @@ function Update-VSTeamWorkItem { [Parameter(Mandatory = $false)] [string]$AssignedTo, + [Parameter(Mandatory = $false)] + [PSCustomObject[]]$Relations, + [Parameter(Mandatory = $false)] [hashtable]$AdditionalFields, @@ -48,6 +51,36 @@ function Update-VSTeamWorkItem { value = $AssignedTo }) | Where-Object { $_.value } + foreach ($relation in $Relations) { + switch ($relation.Operation) { + "add" { + $body += @{ + op = $relation.Operation + path = "/relations/-" + value = @{ + rel = $relation.RelationType + url = _buildRequestURI -area "wit" -resource "workItems" -id $relation.Id + attributes = @{ + comment = $relation.Comment + } + } + } + } + "remove" { + $body += @{ + op = $relation.Operation + path = "/relations/$($relation.Index)" + } + } + "replace" { + $body += @{ + op = $relation.Operation + path = "/relations/$($relation.Index)/attributes/comment" + value = $relation.Comment + } + } + } + } #this loop must always come after the main work item fields defined in the function parameters if ($AdditionalFields) { diff --git a/Source/formats/vsteam_lib.WorkItemRelation.TableView.ps1xml b/Source/formats/vsteam_lib.WorkItemRelation.TableView.ps1xml new file mode 100644 index 000000000..a64615b3c --- /dev/null +++ b/Source/formats/vsteam_lib.WorkItemRelation.TableView.ps1xml @@ -0,0 +1,51 @@ + + + + + vsteam_lib.WorkItemRelation.TableView + + vsteam_lib.WorkItemRelation + + + + + + + + + + + + + + + + + + + + + + + + Id + + + RelationType + + + Operation + + + Index + + + Comment + + + + + + + + \ No newline at end of file diff --git a/Source/formats/vsteam_lib.WorkItemRelationType.TableView.ps1xml b/Source/formats/vsteam_lib.WorkItemRelationType.TableView.ps1xml new file mode 100644 index 000000000..48f461caf --- /dev/null +++ b/Source/formats/vsteam_lib.WorkItemRelationType.TableView.ps1xml @@ -0,0 +1,45 @@ + + + + + vsteam_lib.WorkItemRelationType.TableView + + vsteam_lib.WorkItemRelationType + + + + + + + + + + + + + + + + + + + + + Name + + + ReferenceName + + + Usage + + + Topology + + + + + + + + \ No newline at end of file diff --git a/Source/types/vsteam_lib.WorkItemRelationType.ps1xml b/Source/types/vsteam_lib.WorkItemRelationType.ps1xml new file mode 100644 index 000000000..3e82860ae --- /dev/null +++ b/Source/types/vsteam_lib.WorkItemRelationType.ps1xml @@ -0,0 +1,30 @@ + + + + vsteam_lib.WorkItemRelationType + + + Usage + $this.attributes.usage + + + Topology + $this.attributes.topology + + + PSStandardMembers + + + DefaultDisplayPropertySet + + Name + ReferenceName + Usage + Topology + + + + + + + \ No newline at end of file diff --git a/Tests/SampleFiles/Add-VSTeamWorkItemRelation-Id55.json b/Tests/SampleFiles/Add-VSTeamWorkItemRelation-Id55.json new file mode 100644 index 000000000..beee75ea7 --- /dev/null +++ b/Tests/SampleFiles/Add-VSTeamWorkItemRelation-Id55.json @@ -0,0 +1,54 @@ +{ + "id": 55, + "rev": 2, + "fields": { + "System.AreaPath": "TeamModuleIntegration-9ee352af251", + "System.TeamProject": "TeamModuleIntegration-9ee352af251", + "System.IterationPath": "TeamModuleIntegration-9ee352af251", + "System.WorkItemType": "Task", + "System.State": "To Do", + "System.Reason": "New task", + "System.CreatedDate": "2020-09-07T15:05:42.213Z", + "System.CreatedBy": "Administrator ", + "System.ChangedDate": "2020-09-07T15:05:42.213Z", + "System.ChangedBy": "Administrator ", + "System.Title": "Test", + "Microsoft.VSTS.Common.Priority": 2, + "Microsoft.VSTS.Common.StateChangeDate": "2020-09-07T15:05:42.213Z", + "System.Description": "Unit testing" + }, + "relations": [ + { + "rel": "System.LinkTypes.Hierarchy-Forward", + "url": "https://dev.azure.com/test/317f509f-97e9-4bd2-a5b2-38bf28f9a545/_apis/wit/workItems/70", + "attributes": { + "isLocked": false, + "name": "Child" + } + } + ], + "_links": { + "self": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" + }, + "workItemUpdates": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/updates" + }, + "workItemRevisions": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/revisions" + }, + "workItemHistory": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/history" + }, + "html": { + "href": "http://tfs2017:8080/tfs/web/wi.aspx?pcguid=383b3ccd-9646-4a84-b6af-4a5683c8baf9&id=16" + }, + "workItemType": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/6d9e468a-4961-4b69-a855-96b15e758f63/_apis/wit/workItemTypes/Task" + }, + "fields": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/fields" + } + }, + "url": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" +} \ No newline at end of file diff --git a/Tests/SampleFiles/Get-VSTeamWorkItem-Id16-After.json b/Tests/SampleFiles/Get-VSTeamWorkItem-Id16-After.json new file mode 100644 index 000000000..c797a3a37 --- /dev/null +++ b/Tests/SampleFiles/Get-VSTeamWorkItem-Id16-After.json @@ -0,0 +1,56 @@ +{ + "id": 16, + "rev": 2, + "fields": { + "System.AreaPath": "TeamModuleIntegration-9ee352af251", + "System.TeamProject": "TeamModuleIntegration-9ee352af251", + "System.IterationPath": "TeamModuleIntegration-9ee352af251", + "System.WorkItemType": "Task", + "System.State": "To Do", + "System.Reason": "New task", + "System.CreatedDate": "2020-09-07T15:05:42.213Z", + "System.CreatedBy": "Administrator ", + "System.ChangedDate": "2020-09-07T15:05:42.213Z", + "System.ChangedBy": "Administrator ", + "System.Title": "Test", + "Microsoft.VSTS.Common.Priority": 2, + "Microsoft.VSTS.Common.StateChangeDate": "2020-09-07T15:05:42.213Z", + "System.Description": "Unit testing", + "System.Parent": 80 + }, + "relations": [ + { + "rel": "System.LinkTypes.Hierarchy-Reverse", + "url": "https://dev.azure.com/test/317f509f-97e9-4bd2-a5b2-38bf28f9a545/_apis/wit/workItems/80", + "attributes": { + "isLocked": false, + "comment": "Added from AzFunction", + "name": "Parent" + } + } + ], + "_links": { + "self": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" + }, + "workItemUpdates": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/updates" + }, + "workItemRevisions": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/revisions" + }, + "workItemHistory": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/history" + }, + "html": { + "href": "http://tfs2017:8080/tfs/web/wi.aspx?pcguid=383b3ccd-9646-4a84-b6af-4a5683c8baf9&id=16" + }, + "workItemType": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/6d9e468a-4961-4b69-a855-96b15e758f63/_apis/wit/workItemTypes/Task" + }, + "fields": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/fields" + } + }, + "url": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" +} \ No newline at end of file diff --git a/Tests/SampleFiles/Get-VSTeamWorkItem-Id55-After.json b/Tests/SampleFiles/Get-VSTeamWorkItem-Id55-After.json new file mode 100644 index 000000000..a31b6b139 --- /dev/null +++ b/Tests/SampleFiles/Get-VSTeamWorkItem-Id55-After.json @@ -0,0 +1,56 @@ +{ + "id": 55, + "rev": 2, + "fields": { + "System.AreaPath": "TeamModuleIntegration-9ee352af251", + "System.TeamProject": "TeamModuleIntegration-9ee352af251", + "System.IterationPath": "TeamModuleIntegration-9ee352af251", + "System.WorkItemType": "Task", + "System.State": "To Do", + "System.Reason": "New task", + "System.CreatedDate": "2020-09-07T15:05:42.213Z", + "System.CreatedBy": "Administrator ", + "System.ChangedDate": "2020-09-07T15:05:42.213Z", + "System.ChangedBy": "Administrator ", + "System.Title": "Test", + "Microsoft.VSTS.Common.Priority": 2, + "Microsoft.VSTS.Common.StateChangeDate": "2020-09-07T15:05:42.213Z", + "System.Description": "Unit testing", + "System.Parent": 80 + }, + "relations": [ + { + "rel": "System.LinkTypes.Hierarchy-Reverse", + "url": "https://dev.azure.com/test/317f509f-97e9-4bd2-a5b2-38bf28f9a545/_apis/wit/workItems/80", + "attributes": { + "isLocked": false, + "comment": "Added from AzFunction", + "name": "Parent" + } + } + ], + "_links": { + "self": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" + }, + "workItemUpdates": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/updates" + }, + "workItemRevisions": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/revisions" + }, + "workItemHistory": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/history" + }, + "html": { + "href": "http://tfs2017:8080/tfs/web/wi.aspx?pcguid=383b3ccd-9646-4a84-b6af-4a5683c8baf9&id=16" + }, + "workItemType": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/6d9e468a-4961-4b69-a855-96b15e758f63/_apis/wit/workItemTypes/Task" + }, + "fields": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/fields" + } + }, + "url": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" +} \ No newline at end of file diff --git a/Tests/SampleFiles/Get-VSTeamWorkItem-Id55-Before.json b/Tests/SampleFiles/Get-VSTeamWorkItem-Id55-Before.json new file mode 100644 index 000000000..b9a09510e --- /dev/null +++ b/Tests/SampleFiles/Get-VSTeamWorkItem-Id55-Before.json @@ -0,0 +1,56 @@ +{ + "id": 55, + "rev": 1, + "fields": { + "System.AreaPath": "TeamModuleIntegration-9ee352af251", + "System.TeamProject": "TeamModuleIntegration-9ee352af251", + "System.IterationPath": "TeamModuleIntegration-9ee352af251", + "System.WorkItemType": "Task", + "System.State": "To Do", + "System.Reason": "New task", + "System.CreatedDate": "2020-09-07T15:05:42.213Z", + "System.CreatedBy": "Administrator ", + "System.ChangedDate": "2020-09-07T15:05:42.213Z", + "System.ChangedBy": "Administrator ", + "System.Title": "Test", + "Microsoft.VSTS.Common.Priority": 2, + "Microsoft.VSTS.Common.StateChangeDate": "2020-09-07T15:05:42.213Z", + "System.Description": "Unit testing", + "System.Parent": 70 + }, + "relations": [ + { + "rel": "System.LinkTypes.Hierarchy-Reverse", + "url": "https://dev.azure.com/test/317f509f-97e9-4bd2-a5b2-38bf28f9a545/_apis/wit/workItems/70", + "attributes": { + "isLocked": false, + "comment": "Added from AzFunction", + "name": "Parent" + } + } + ], + "_links": { + "self": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" + }, + "workItemUpdates": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/updates" + }, + "workItemRevisions": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/revisions" + }, + "workItemHistory": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16/history" + }, + "html": { + "href": "http://tfs2017:8080/tfs/web/wi.aspx?pcguid=383b3ccd-9646-4a84-b6af-4a5683c8baf9&id=16" + }, + "workItemType": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/6d9e468a-4961-4b69-a855-96b15e758f63/_apis/wit/workItemTypes/Task" + }, + "fields": { + "href": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/fields" + } + }, + "url": "http://tfs2017:8080/tfs/DefaultCollection/_apis/wit/workItems/16" +} \ No newline at end of file diff --git a/Tests/SampleFiles/Get-VSTeamWorkItemRelationType.json b/Tests/SampleFiles/Get-VSTeamWorkItemRelationType.json new file mode 100644 index 000000000..23cc5de54 --- /dev/null +++ b/Tests/SampleFiles/Get-VSTeamWorkItemRelationType.json @@ -0,0 +1,273 @@ +{ + "count": 18, + "value": [ + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": true, + "oppositeEndReferenceName": "Microsoft.VSTS.Common.Affects-Reverse" + }, + "referenceName": "Microsoft.VSTS.Common.Affects-Forward", + "name": "Affects", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.Common.Affects-Forward" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": false, + "oppositeEndReferenceName": "Microsoft.VSTS.Common.Affects-Forward" + }, + "referenceName": "Microsoft.VSTS.Common.Affects-Reverse", + "name": "Affected By", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.Common.Affects-Reverse" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": true, + "oppositeEndReferenceName": "Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Reverse" + }, + "referenceName": "Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Forward", + "name": "Referenced By", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Forward" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": false, + "oppositeEndReferenceName": "Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Forward" + }, + "referenceName": "Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Reverse", + "name": "References", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Reverse" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": true, + "oppositeEndReferenceName": "Microsoft.VSTS.Common.TestedBy-Reverse" + }, + "referenceName": "Microsoft.VSTS.Common.TestedBy-Forward", + "name": "Tested By", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.Common.TestedBy-Forward" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": false, + "oppositeEndReferenceName": "Microsoft.VSTS.Common.TestedBy-Forward" + }, + "referenceName": "Microsoft.VSTS.Common.TestedBy-Reverse", + "name": "Tests", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.Common.TestedBy-Reverse" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": true, + "oppositeEndReferenceName": "Microsoft.VSTS.TestCase.SharedStepReferencedBy-Reverse" + }, + "referenceName": "Microsoft.VSTS.TestCase.SharedStepReferencedBy-Forward", + "name": "Test Case", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.TestCase.SharedStepReferencedBy-Forward" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": true, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": false, + "oppositeEndReferenceName": "Microsoft.VSTS.TestCase.SharedStepReferencedBy-Forward" + }, + "referenceName": "Microsoft.VSTS.TestCase.SharedStepReferencedBy-Reverse", + "name": "Shared Steps", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Microsoft.VSTS.TestCase.SharedStepReferencedBy-Reverse" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": false, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": false, + "topology": "tree", + "isForward": true, + "oppositeEndReferenceName": "System.LinkTypes.Duplicate-Reverse" + }, + "referenceName": "System.LinkTypes.Duplicate-Forward", + "name": "Duplicate", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/System.LinkTypes.Duplicate-Forward" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": false, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": false, + "topology": "tree", + "isForward": false, + "oppositeEndReferenceName": "System.LinkTypes.Duplicate-Forward" + }, + "referenceName": "System.LinkTypes.Duplicate-Reverse", + "name": "Duplicate Of", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/System.LinkTypes.Duplicate-Reverse" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": false, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": true, + "oppositeEndReferenceName": "System.LinkTypes.Dependency-Reverse" + }, + "referenceName": "System.LinkTypes.Dependency-Forward", + "name": "Successor", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/System.LinkTypes.Dependency-Forward" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": false, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": true, + "topology": "dependency", + "isForward": false, + "oppositeEndReferenceName": "System.LinkTypes.Dependency-Forward" + }, + "referenceName": "System.LinkTypes.Dependency-Reverse", + "name": "Predecessor", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/System.LinkTypes.Dependency-Reverse" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": false, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": false, + "topology": "tree", + "isForward": true, + "oppositeEndReferenceName": "System.LinkTypes.Hierarchy-Reverse" + }, + "referenceName": "System.LinkTypes.Hierarchy-Forward", + "name": "Child", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/System.LinkTypes.Hierarchy-Forward" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": false, + "enabled": true, + "acyclic": true, + "directional": true, + "singleTarget": false, + "topology": "tree", + "isForward": false, + "oppositeEndReferenceName": "System.LinkTypes.Hierarchy-Forward" + }, + "referenceName": "System.LinkTypes.Hierarchy-Reverse", + "name": "Parent", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/System.LinkTypes.Hierarchy-Reverse" + }, + { + "attributes": { + "usage": "workItemLink", + "editable": false, + "enabled": true, + "acyclic": false, + "directional": false, + "singleTarget": true, + "topology": "network" + }, + "referenceName": "System.LinkTypes.Related", + "name": "Related", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/System.LinkTypes.Related" + }, + { + "attributes": { + "usage": "resourceLink", + "editable": false, + "enabled": true + }, + "referenceName": "AttachedFile", + "name": "Attached File", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/AttachedFile" + }, + { + "attributes": { + "usage": "resourceLink", + "editable": false, + "enabled": true + }, + "referenceName": "Hyperlink", + "name": "Hyperlink", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/Hyperlink" + }, + { + "attributes": { + "usage": "resourceLink", + "editable": false, + "enabled": true + }, + "referenceName": "ArtifactLink", + "name": "Artifact Link", + "url": "https://dev.azure.com/test/_apis/wit/workItemRelationTypes/ArtifactLink" + } + ] + } \ No newline at end of file diff --git a/Tests/SampleFiles/Get-VSTeamWorkItemRelationTypePSObject.json b/Tests/SampleFiles/Get-VSTeamWorkItemRelationTypePSObject.json new file mode 100644 index 000000000..6c719cbb9 --- /dev/null +++ b/Tests/SampleFiles/Get-VSTeamWorkItemRelationTypePSObject.json @@ -0,0 +1,110 @@ +[ + { + "name": "Affects", + "referenceName": "Microsoft.VSTS.Common.Affects-Forward", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Affected By", + "referenceName": "Microsoft.VSTS.Common.Affects-Reverse", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Produces For", + "referenceName": "System.LinkTypes.Remote.Dependency-Forward", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Consumes From", + "referenceName": "System.LinkTypes.Remote.Dependency-Reverse", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Referenced By", + "referenceName": "Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Forward", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "References", + "referenceName": "Microsoft.VSTS.TestCase.SharedParameterReferencedBy-Reverse", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Tested By", + "referenceName": "Microsoft.VSTS.Common.TestedBy-Forward", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Tests", + "referenceName": "Microsoft.VSTS.Common.TestedBy-Reverse", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Test Case", + "referenceName": "Microsoft.VSTS.TestCase.SharedStepReferencedBy-Forward", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Shared Steps", + "referenceName": "Microsoft.VSTS.TestCase.SharedStepReferencedBy-Reverse", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Duplicate", + "referenceName": "System.LinkTypes.Duplicate-Forward", + "Usage": "workItemLink", + "Topology": "tree" + }, + { + "name": "Duplicate Of", + "referenceName": "System.LinkTypes.Duplicate-Reverse", + "Usage": "workItemLink", + "Topology": "tree" + }, + { + "name": "Successor", + "referenceName": "System.LinkTypes.Dependency-Forward", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Predecessor", + "referenceName": "System.LinkTypes.Dependency-Reverse", + "Usage": "workItemLink", + "Topology": "dependency" + }, + { + "name": "Child", + "referenceName": "System.LinkTypes.Hierarchy-Forward", + "Usage": "workItemLink", + "Topology": "tree" + }, + { + "name": "Parent", + "referenceName": "System.LinkTypes.Hierarchy-Reverse", + "Usage": "workItemLink", + "Topology": "tree" + }, + { + "name": "Related", + "referenceName": "System.LinkTypes.Related", + "Usage": "workItemLink", + "Topology": "network" + }, + { + "name": "Remote Related", + "referenceName": "System.LinkTypes.Remote.Related", + "Usage": "workItemLink", + "Topology": "network" + } + ] \ No newline at end of file diff --git a/Tests/function/tests/Add-VSTeamWorkItemRelation.Tests.ps1 b/Tests/function/tests/Add-VSTeamWorkItemRelation.Tests.ps1 new file mode 100644 index 000000000..18f8e83f3 --- /dev/null +++ b/Tests/function/tests/Add-VSTeamWorkItemRelation.Tests.ps1 @@ -0,0 +1,26 @@ +Set-StrictMode -Version Latest + +Describe "VSTeamWorkItemRelation" { + BeforeAll { + . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath + . "$baseFolder/Source/Public/Update-VSTeamWorkItem.ps1" + . "$baseFolder/Source/Public/New-VSTeamWorkItemRelation.ps1" + + Mock _getInstance { return 'https://dev.azure.com/test' } + Mock _getApiVersion { return '1.0-unitTests' } + Mock Invoke-RestMethod { Open-SampleFile 'Add-VSTeamWorkItemRelation-Id55.json' } -ParameterFilter { $Uri -like '*/_apis/wit/workitems/55*' -and $Method -eq 'Patch' } + Mock New-VSTeamWorkItemRelation { return [PSCustomObject]@{ Id = 70; RelationType = 'System.LinkTypes.Hierarchy-Reverse'; Operation = 'add'; Index = '-' }} -ParameterFilter { $id -eq 70 } + } + + Context 'Add-VSTeamWorkItemRelation' { + It 'add work items as related of another work item and returns the related work item' { + $actual = Add-VSTeamWorkItemRelation -Id 70 -RelationType Child -OfRelatedId 55 + + $actual.Relations | Should -HaveCount 1 + $actual.Relations[0].url.split('/')[-1] | Should -Be 70 + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It + Should -Invoke New-VSTeamWorkItemRelation -Exactly -Times 1 -Scope It + } + + } +} \ No newline at end of file diff --git a/Tests/function/tests/Get-VSTeamWorkItemRelation.Tests.ps1 b/Tests/function/tests/Get-VSTeamWorkItemRelation.Tests.ps1 new file mode 100644 index 000000000..7bfb815a7 --- /dev/null +++ b/Tests/function/tests/Get-VSTeamWorkItemRelation.Tests.ps1 @@ -0,0 +1,33 @@ +Set-StrictMode -Version Latest + +Describe 'VSTeamWorkItemRelation' { + BeforeAll { + . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath + . "$baseFolder/Source/Public/New-VSTeamWorkItemRelation.ps1" + . "$baseFolder/Source/Public/Get-VSTeamWorkItem.ps1" + . "$baseFolder/Source/Public/Get-VSTeamWorkItemRelationType.ps1" + + + Mock _getInstance { return 'https://dev.azure.com/test' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id55-After.json' } + Mock _getApiVersion { return '5.0-unitTests' } -ParameterFilter { $Service -eq 'Core' } + Mock New-VSTeamWorkItemRelation { return [PSCustomObject]@{ Id = 80; RelationType = 'System.LinkTypes.Hierarchy-Reverse'; Operation = 'add'; Index = '-'; Comment = 'Added from AzFunction' }} -ParameterFilter { $id -eq 80 } + + + } + + Context 'Get-VSTeamWorkItemRelation' { + It 'Should return relations' { + ## Act + $relation = Get-VSTeamWorkItemRelation -Id 55 + + ## Assert + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + $Uri -eq "https://dev.azure.com/test/_apis/wit/workitems/55?api-version=$(_getApiVersion Core)&`$Expand=Relations" + } + $relation.Id | Should -Be 80 + $relation.RelationType | Should -Be "System.LinkTypes.Hierarchy-Reverse" + $relation.Comment | Should -Be "Added from AzFunction" + } + } +} \ No newline at end of file diff --git a/Tests/function/tests/Get-VSTeamWorkItemRelationType.Tests.ps1 b/Tests/function/tests/Get-VSTeamWorkItemRelationType.Tests.ps1 new file mode 100644 index 000000000..da6c6e4aa --- /dev/null +++ b/Tests/function/tests/Get-VSTeamWorkItemRelationType.Tests.ps1 @@ -0,0 +1,31 @@ +Set-StrictMode -Version Latest + +Describe 'VSTeamWorkItemRelationType' { + BeforeAll { + . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath + + Mock _getInstance { return 'https://dev.azure.com/test' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItemRelationType.json' } + Mock _getApiVersion { return '5.0-unitTests' } -ParameterFilter { $Service -eq 'Core' } + } + + Context 'Get-VSTeamWorkItemRelationType' { + It 'Should return relations default to WorkItemLink' { + ## Act + Get-VSTeamWorkItemRelationType + + ## Assert + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + $Uri -eq "https://dev.azure.com/test/_apis/wit/workitemrelationtypes?api-version=$(_getApiVersion Core)" + } + } + + It 'With All parameter should return 2 relation types' { + ## Act + $relationTypes = Get-VSTeamWorkItemRelationType -Usage All + + ## Assert + $relationTypes | Select-Object -ExpandProperty attributes | Select-Object Usage -Unique | Should -HaveCount 2 + } + } +} \ No newline at end of file diff --git a/Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 b/Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 new file mode 100644 index 000000000..e6aa48ee8 --- /dev/null +++ b/Tests/function/tests/New-VSTeamWorkItemRelation.Tests.ps1 @@ -0,0 +1,86 @@ +Set-StrictMode -Version Latest + +Describe 'VSTeamWorkItemRelation' { + BeforeAll { + . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath + . "$baseFolder/Source/Public/Get-VSTeamWorkItem.ps1" + + Mock _getInstance { return 'https://dev.azure.com/test' } + Mock _getApiVersion { return '5.0-unitTests' } -ParameterFilter { $Service -eq 'Core' } + Mock Get-VSTeamWorkItem { + return [PSCustomObject]@{ + Id = 55 + Title = "my title" + } + } + + $relations = Open-SampleFile 'Get-VSTeamWorkItemRelationTypePSObject.json' + $relationsDict = [System.Collections.Generic.Dictionary[string, string]]::new() + foreach ($r in $relations) { + $relationsDict.Add($r.Name, $r.ReferenceName) + } + [vsteam_lib.RelationTypeCache]::Update($relationsDict); + + } + + Context 'New-VSTeamWorkItemRelation' { + It 'Sould return a Relation object' { + $expected = [PSCustomObject]@{ + Id = 55 + Operation = "add" + RelationType = "System.LinkTypes.Hierarchy-Reverse" + Comment = "MyComment" + } + + $actual = New-VSTeamWorkItemRelation -RelationType Parent -Id 55 -Comment "MyComment" + + $actual.Id | Should -Be $expected.Id + $actual.Operation | Should -Be $expected.Operation + $actual.RelationType | Should -Be $expected.RelationType + $actual.Comment | Should -Be $expected.Comment + } + + + It 'With list pipeline should return an array of relations' { + $actual = 55, 66, 77 | New-VSTeamWorkItemRelation -RelationType Child + + $actual | Should -HaveCount 3 + $actual[0].Id | Should -Be "55" + $actual[1].Id | Should -Be "66" + $actual[2].Id | Should -Be "77" + $actual[0].RelationType | Should -Be "System.LinkTypes.Hierarchy-Forward" + } + + It 'With chained calls should return an array of relations' { + $actual = New-VSTeamWorkItemRelation -RelationType Child -Id 55 | + New-VSTeamWorkItemRelation -RelationType Child -Id 66 | + New-VSTeamWorkItemRelation -RelationType Child -Id 77 + + $actual | Should -HaveCount 3 + $actual[0].Id | Should -Be "55" + $actual[1].Id | Should -Be "66" + $actual[2].Id | Should -Be "77" + $actual[0].RelationType | Should -Be "System.LinkTypes.Hierarchy-Forward" + } + + It 'With WorkItem in the pipeline should use the workItem.Id' { + $actual = Get-VSTeamWorkItem -Id 55 | New-VSTeamWorkItemRelation -RelationType Child + $actual.Id | Should -Be "55" + $actual.Operation | Should -Be "add" + $actual.RelationType | Should -Be "System.LinkTypes.Hierarchy-Forward" + } + + It 'Objects have vsteam_lib.WorkItemRelation TypeName' { + $actual = New-VSTeamWorkItemRelation -RelationType Child -Id 55 | + New-VSTeamWorkItemRelation -RelationType Child -Id 66 + $actual[0].PSObject.TypeNames | Should -Contain "vsteam_lib.WorkItemRelation" + $actual[1].PSObject.TypeNames | Should -Contain "vsteam_lib.WorkItemRelation" + } + + It 'With Index paramter ID nor RelationType are needed' { + $actual = New-VSTeamWorkItemRelation -Index 0 -Operation Remove + $actual.Id | Should -BeNullOrEmpty + $actual.RelationType | Should -BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/Tests/function/tests/Remove-VSTeamWorkItemRelation.Tests.ps1 b/Tests/function/tests/Remove-VSTeamWorkItemRelation.Tests.ps1 new file mode 100644 index 000000000..aad496a33 --- /dev/null +++ b/Tests/function/tests/Remove-VSTeamWorkItemRelation.Tests.ps1 @@ -0,0 +1,39 @@ +Set-StrictMode -Version Latest + +Describe 'VSTeamWorkItemRelation' { + BeforeAll { + . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath + . "$baseFolder/Source/Public/Get-VSTeamWorkItem.ps1" + . "$baseFolder/Source/Public/Get-VSTeamWorkItemRelation.ps1" + . "$baseFolder/Source/Public/New-VSTeamWorkItemRelation.ps1" + . "$baseFolder/Source/Public/Update-VSTeamWorkItem.ps1" + + Mock _getInstance { return 'https://dev.azure.com/test' } + Mock _getApiVersion { return '5.0-unitTests' } -ParameterFilter { $Service -eq 'Core' } + Mock New-VSTeamWorkItemRelation { return [PSCustomObject]@{ Id = 80; RelationType = 'System.LinkTypes.Hierarchy-Reverse'; Operation = 'remove'; Index = '0' }} + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id16-After.json' } -ParameterFilter { $Id -eq 16 -and $Relations -eq $null } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id16.json' } -ParameterFilter { $Id -eq 16 -and $Relations -ne $null } + + } + + Context 'Remove-VSTeamWorkItemRelation' { + It 'Should remove relations' { + ## Act + $workItem = Remove-VSTeamWorkItemRelation -Id 16 -FromRelatedId 80 + + ## Assert + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { + $Uri -eq "https://dev.azure.com/test/_apis/wit/workitems/16?api-version=$(_getApiVersion Core)&`$Expand=Relations" + } + Should -Invoke New-VSTeamWorkItemRelation -Exactly -Times 2 -Scope It + Should -Invoke Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Method -eq 'Patch' -and + $ContentType -eq 'application/json-patch+json; charset=utf-8' -and + $Uri -eq "https://dev.azure.com/test/_apis/wit/workitems/16?api-version=$(_getApiVersion Core)" + } + + $workItem.Id | Should -Be 16 + $workItem.Relations | Should -BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/Tests/function/tests/Switch-VSTeamWorkItemParent.Tests.ps1 b/Tests/function/tests/Switch-VSTeamWorkItemParent.Tests.ps1 new file mode 100644 index 000000000..c7bacb113 --- /dev/null +++ b/Tests/function/tests/Switch-VSTeamWorkItemParent.Tests.ps1 @@ -0,0 +1,57 @@ +Set-StrictMode -Version Latest + +Describe "VSTeamWorkItemParent" { + BeforeAll { + . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath + . "$baseFolder/Source/Public/Get-VSTeamWorkItem.ps1" + . "$baseFolder/Source/Public/Update-VSTeamWorkItem.ps1" + . "$baseFolder/Source/Public/New-VSTeamWorkItemRelation.ps1" + + Mock _getInstance { return 'https://dev.azure.com/test' } + Mock _getApiVersion { return '1.0-unitTests' } #-ParameterFilter { $Service -eq 'Core' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id16.json' } -ParameterFilter { $Uri -like '*/_apis/wit/workitems/16*' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id16-After.json' } -ParameterFilter { $Uri -like '*/_apis/wit/workitems/16*' -and $Method -eq 'Patch' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id55-Before.json' } -ParameterFilter { $Uri -like '*/_apis/wit/workitems/55*' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id55-After.json' } -ParameterFilter { $Uri -like '*/_apis/wit/workitems/55*' -and $Method -eq 'Patch' } + Mock New-VSTeamWorkItemRelation { return [PSCustomObject]@{ Operation = 'remove'; Index = 0 }} -ParameterFilter { $index -eq 0 } + Mock New-VSTeamWorkItemRelation { return [PSCustomObject]@{ Id = 80; RelationType = 'System.LinkTypes.Hierarchy-Reverse'; Operation = 'add'; Index = '-' }} -ParameterFilter { $id -eq 80 } + Mock New-VSTeamWorkItemRelation { return @( + [PSCustomObject]@{ Operation = 'remove'; Index = 0 } + [PSCustomObject]@{ Id = 80; RelationType = 'System.LinkTypes.Hierarchy-Reverse'; Operation = 'add'; Index = '-' } + )} -ParameterFilter { $id -eq 80 -and $InputObject -ne $null} + } + + Context 'Switch-VSTeamWorkItemParent' { + It 'replaces old parent with new one' { + $actual = Switch-VSTeamWorkItemParent -Id 55 -ParentId 80 + + $actual.Fields."System.Parent" | Should -Be 80 + Should -Invoke Invoke-RestMethod -Exactly -Times 2 -Scope It + Should -Invoke New-VSTeamWorkItemRelation -Exactly -Times 2 -Scope It + } + It 'should not replace the parent if new parent is the same than old parent' { + $actual = Switch-VSTeamWorkItemParent -Id 55 -ParentId 70 + + $actual.Fields."System.Parent" | Should -Be 70 + Should -Invoke New-VSTeamWorkItemRelation -Exactly -Times 0 -Scope It + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It -ParameterFilter { $Uri -like '*/_apis/wit/workitems/55*' } + } + + It 'with AddParent it should add the parent if workitem did not have one' { + $actual = Switch-VSTeamWorkItemParent -Id 16 -ParentId 80 -AddParent + + $actual.Fields."System.Parent" | Should -Be 80 + Should -Invoke Invoke-RestMethod -Exactly -Times 2 -Scope It + Should -Invoke New-VSTeamWorkItemRelation -Exactly -Times 1 -Scope It + } + + It 'without AddParent it should NOT add the parent if workitem did not have one' { + $actual = Switch-VSTeamWorkItemParent -Id 16 -ParentId 80 + + $actual.Fields."System.Parent" | Should -BeNullOrEmpty + Should -Invoke Invoke-RestMethod -Exactly -Times 1 -Scope It + Should -Invoke New-VSTeamWorkItemRelation -Exactly -Times 0 -Scope It + } + + } +} \ No newline at end of file diff --git a/Tests/function/tests/Update-VSTeamWorkItem.Tests.ps1 b/Tests/function/tests/Update-VSTeamWorkItem.Tests.ps1 index b03866801..4854629f4 100644 --- a/Tests/function/tests/Update-VSTeamWorkItem.Tests.ps1 +++ b/Tests/function/tests/Update-VSTeamWorkItem.Tests.ps1 @@ -3,6 +3,7 @@ Set-StrictMode -Version Latest Describe 'VSTeamWorkItem' { BeforeAll { . "$PSScriptRoot\_testInitialize.ps1" $PSCommandPath + . "$baseFolder/Source/Public/New-VSTeamWorkItemRelation.ps1" Mock _getInstance { return 'https://dev.azure.com/test' } } @@ -10,6 +11,8 @@ Describe 'VSTeamWorkItem' { Context 'Update-VSTeamWorkItem' { BeforeAll { Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id16.json' } + Mock Invoke-RestMethod { Open-SampleFile 'Get-VSTeamWorkItem-Id16-After.json' } -ParameterFilter { $relations -ne $null } + Mock New-VSTeamWorkItemRelation { return [PSCustomObject]@{ Id = 80; RelationType = 'System.LinkTypes.Hierarchy-Reverse'; Operation = 'add'; Index = '-' }} -ParameterFilter { $id -eq 80 } } It 'Without Default Project should update work item' { @@ -124,5 +127,30 @@ Describe 'VSTeamWorkItem' { ## Act / Assert { Update-VSTeamWorkItem 1 -Title Test1 -Description Testing -AdditionalFields $additionalFields } | Should -Throw } + + It 'With relations should update relations' { + ## Arrange + $Global:PSDefaultParameterValues["*-vsteam*:projectName"] = 'test' + $relations = New-VSTeamWorkItemRelation -Id 80 -RelationType Parent + + ## Act / Assert + $wi = Update-VSTeamWorkItem 1 -Title Test1 -Description Testing -Relations $relations -Force + $wi.Fields."System.Parent" | Should -Be 80 + Should -Invoke Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Method -eq 'Patch' -and + $Body -like '`[*' -and # Make sure the body is an array + $Body -like '*Test1*' -and + $Body -like '*Testing*' -and + $Body -like '*/fields/System.Title*' -and + $Body -like '*/fields/System.Description*' -and + $Body -like '*/relations/-*' -and + $Body -like '*`]' -and # Make sure the body is an array + $ContentType -eq 'application/json-patch+json; charset=utf-8' -and + $Uri -eq "https://dev.azure.com/test/_apis/wit/workitems/1?api-version=$(_getApiVersion Core)" + } + + + + } } } \ No newline at end of file diff --git a/Tests/library/Attribute/RelationTypeToReferenceNameAttributeTests.cs b/Tests/library/Attribute/RelationTypeToReferenceNameAttributeTests.cs new file mode 100644 index 000000000..ff0fdaa63 --- /dev/null +++ b/Tests/library/Attribute/RelationTypeToReferenceNameAttributeTests.cs @@ -0,0 +1,53 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Management.Automation; + +namespace vsteam_lib.Test +{ + [TestClass] + [ExcludeFromCodeCoverage] + public class RelationTypeToReferenceNameAttributeTest + { + + private readonly Collection _relationNames = new Collection() { "Produces For", "Predecessor", "Parent" }; + private readonly Collection _relations = new Collection() { + PSObject.AsPSObject(new {Name = "Produces For", ReferenceName = "System.LinkTypes.Remote.Dependency-Forward" }), + PSObject.AsPSObject(new {Name = "Predecessor", ReferenceName = "System.LinkTypes.Dependency-Reverse" }), + PSObject.AsPSObject(new {Name = "Parent", ReferenceName = "System.LinkTypes.Hierarchy-Reverse" }) + }; + + [TestMethod] + public void RelationTypeTransformToReferenceNameAttribute_Null() + { + // Arrange + string expected = null; + var target = new RelationTypeToReferenceNameAttribute(); + + // Act + var actual = target.Transform(null, null); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + [ExpectedException(typeof(KeyNotFoundException))] + public void RelationTypeTransformToReferenceNameAttribute_Name_Not_Found() + { + // Arrange + var target = new RelationTypeToReferenceNameAttribute(); + + var ps = BaseTests.PrepPowerShell(); + ps.Invoke().Returns(this._relations); + ps.Invoke(this._relations).Returns(this._relationNames); + RelationTypeCache.Cache.Shell = ps; + + // Act + var actual = target.Transform(null, "NonExistingName"); + } + + } +} diff --git a/Tests/library/Cache/RelationTypeCacheTests.cs b/Tests/library/Cache/RelationTypeCacheTests.cs new file mode 100644 index 000000000..a1225d0df --- /dev/null +++ b/Tests/library/Cache/RelationTypeCacheTests.cs @@ -0,0 +1,148 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Management.Automation; + +namespace vsteam_lib.Test +{ + [TestClass] + [ExcludeFromCodeCoverage] + public class RelationTypeCacheTests + { + private readonly Collection _empty = new Collection(); + private readonly Collection _emptyStrings = new Collection(); + private readonly Collection _relationNames = new Collection() { "Produces For", "Predecessor", "Parent" }; + private readonly Collection _relations = new Collection() { + PSObject.AsPSObject(new {Name = "Produces For", ReferenceName = "System.LinkTypes.Remote.Dependency-Forward" }), + PSObject.AsPSObject(new {Name = "Predecessor", ReferenceName = "System.LinkTypes.Dependency-Reverse" }), + PSObject.AsPSObject(new {Name = "Parent", ReferenceName = "System.LinkTypes.Hierarchy-Reverse" }) + }; + + [TestMethod] + public void RelationTypeCache_HasCacheExpired() + { + // Arrange + var expected = true; + + // Act + RelationTypeCache.Invalidate(); + + // Assert + Assert.AreEqual(expected, RelationTypeCache.HasCacheExpired, "Cache should be expired"); + + // Act + RelationTypeCache.Update(new Dictionary()); + + // Assert + Assert.AreNotEqual(expected, RelationTypeCache.HasCacheExpired, "Cache should not be expired"); + } + + [TestMethod] + public void RelationTypeCache_Update_With_Null_List() + { + // Arrange + int expected = 3; + var ps = BaseTests.PrepPowerShell(); + ps.Invoke().Returns(this._relations); + ps.Invoke(this._relations).Returns(this._relationNames); + RelationTypeCache.Cache.Shell = ps; + + // Act + RelationTypeCache.Update(null); + + // Assert + Assert.AreEqual(expected, RelationTypeCache.Cache.Values.Count); + } + + [TestMethod] + public void RelationTypeCache_GetCurrent() + { + // Arrange + var expected = 3; + var ps = BaseTests.PrepPowerShell(); + ps.Invoke().Returns(this._relations); + ps.Invoke(this._relations).Returns(this._relationNames); + + ps.Invoke().Returns(this._relationNames); + RelationTypeCache.Invalidate(); + RelationTypeCache.Cache.Shell = ps; + + // Act + var actual = RelationTypeCache.GetCurrent(); + + // Assert + Assert.AreEqual(expected, actual.Count()); + } + + [TestMethod] + public void RelationTypeCache_Update_Returns_Null() + { + // Arrange + var expected = 0; + var ps = BaseTests.PrepPowerShell(); + ps.Invoke().Returns(this._empty); + ps.Invoke().Returns(_emptyStrings); + RelationTypeCache.Cache.Shell = ps; + + // Act + RelationTypeCache.Update(null); + + // Assert + Assert.AreEqual(expected, RelationTypeCache.Cache.Values.Count); + } + + [TestMethod] + public void RelationTypeCache_Update_With_Empty_List() + { + // Arrange + var expected = 0; + var ps = BaseTests.PrepPowerShell(); + RelationTypeCache.Cache.Shell = ps; + + // Act + RelationTypeCache.Update(new Dictionary()); + + // Assert + Assert.AreEqual(expected, RelationTypeCache.Cache.Values.Count); + } + + [TestMethod] + public void RelationTypeCache_Get_ReferenceName() + { + // Arrange + var ps = BaseTests.PrepPowerShell(); + ps.Invoke().Returns(this._relations); + ps.Invoke().Returns(this._relationNames); + RelationTypeCache.Cache.Shell = ps; + + var expected = "System.LinkTypes.Hierarchy-Reverse"; + + // Force an update + RelationTypeCache.Update(null); + + // Act + var actual = RelationTypeCache.GetReferenceName("Parent"); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + [ExpectedException(typeof(KeyNotFoundException))] + public void RelationTypeCache_Get_ReferenceName_Throws_When_Not_Found() + { + // Arrange + RelationTypeCache.Update(new Dictionary() { + { "key1", "value1"}, + { "key2", "value2"} + }); + + // Act + var actual = RelationTypeCache.GetReferenceName("NonExistingName"); + + } + } +} diff --git a/Tests/library/Completer/WorkItemRelationTypeCompleterTest.cs b/Tests/library/Completer/WorkItemRelationTypeCompleterTest.cs new file mode 100644 index 000000000..218682449 --- /dev/null +++ b/Tests/library/Completer/WorkItemRelationTypeCompleterTest.cs @@ -0,0 +1,60 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Management.Automation; + +namespace vsteam_lib.Test +{ + [TestClass] + [ExcludeFromCodeCoverage] + public class WorkItemRelationTypeCompleterTest + { + + private readonly Collection _values = new Collection() { "Produces For", "Predecessor", "Parent" }; + + private readonly Collection _relationObjects = new Collection() { + PSObject.AsPSObject(new {Name = "Produces For", ReferenceName = "System.LinkTypes.Remote.Dependency-Forward" }), + PSObject.AsPSObject(new {Name = "Predecessor", ReferenceName = "System.LinkTypes.Dependency-Reverse" }), + PSObject.AsPSObject(new {Name = "Parent", ReferenceName = "System.LinkTypes.Hierarchy-Reverse" }) + }; + + + private readonly Dictionary _relations = new Dictionary { + + { "'Produces For'", "System.LinkTypes.Remote.Dependency-Forward" }, + { "Predecessor", "System.LinkTypes.Dependency-Reverse" }, + { "Parent", "System.LinkTypes.Hierarchy-Reverse" } + }; + + [TestMethod] + public void WorkImteRelations_Exercise() + { + // Arrange + var ps = BaseTests.PrepPowerShell(); + ps.Invoke().Returns(this._relationObjects); + ps.Invoke(this._relationObjects).Returns(this._values); + RelationTypeCache.Cache.Shell = ps; + RelationTypeCache.Invalidate(); + + var target = new WorkItemRelationTypeCompleter(ps); + var fakeBoundParameters = new Dictionary(); + + // Act + var actual = target.CompleteArgument(string.Empty, string.Empty, "P", null, fakeBoundParameters); + + // Assert + Assert.AreEqual(_relations.Count, actual.Count()); + var e = actual.GetEnumerator(); + foreach (var relation in _relations) + { + e.MoveNext(); + Assert.AreEqual(relation.Key, e.Current.CompletionText); + } + + + } + } +} diff --git a/config.json b/config.json index 17643213e..7511f790f 100644 --- a/config.json +++ b/config.json @@ -128,7 +128,9 @@ "vsteam_lib.Build.ListView.ps1xml", "vsteam_lib.ResourceArea.TableView.ps1xml", "vsteam_lib.PullRequest.TableView.ps1xml", - "vsteam_lib.Policy.TableView.ps1xml" + "vsteam_lib.Policy.TableView.ps1xml", + "vsteam_lib.WorkItemRelationType.TableView.ps1xml", + "vsteam_lib.WorkItemRelation.TableView.ps1xml" ] } } \ No newline at end of file