Fixing Azure DevOps Test Plan copy results in “clone operation failed”

There was a situation whereby the creation of a new Test Plan failed and resulted in the error “clone operation failed”.

Research showed that there were no problems with test case 69200, but it was linked to a parent (of the test plan). This test plan itself had 535 links, both test cases and test suites and when creating a duplicate of this test plan then duplicates of these cases and test suites were created, all with the test plan as parent. This resulted then in reaching the limit of 1000 links for this test plan. Because of the copy operation that failed in the middle the resulting test plan appeared empty and was thus unusable.

There is no way to do this manually, unless opening and changing all the links manually.

After some research I found some examples that guided me in the direction of the solution, but it required some tweaking.

Steps:

  1. Delete the resulting (wrong) test plan manually in the UI of Azure DevOps
  2. In the top right of Azure DevOps go to User Settings > Personal Access Tokens
  3. Press “New Token”
  4. Give it the name of your choice, enter your organization as it appears as a prefix in the URL of your Azure DevOps account. Select full rights for the sections Work Items and Test Management.
  5. Press Create. A personal access token will be created. Copy this string.
  6. In Azure DevOps create a query whereby you search for the new test cases (or other work items) that have the original test case as parent AND set the date to today (or the actual day that the copy operation failed).
  7. Export the results to CSV file
  8. Create a PowerShell script to delete the newly created test cases and execute it.
  9. In Azure DevOps create a query whereby you search for the new test cases (or other work items) that have the original test case as parent (since we already searched for the ones created before today we focus now on the remaining items).
  10. Export the results to CSV file
  11. Create a PowerShell script to remove the link between these test cases and the Parent test plan and execute it.
  12. In the UI of the Azure DevOps create now a new test plan based on an existing. The copy will be made. This can take a few minutes, based on the size of the test plan, but afterwards it can be consulted as the original test plan. Thus, Azure DevOps keeps a link between the test plan, the suites and test cases, which is not visible in the UI.

Script to remove the duplicate
test cases

$pat = the personal access token created in step 5
$organization = the name of your organization and your project, e.g. company_x/project_y
$csvData = the path to the CSVfile that was created in step 7

# Your Personal Access Token (PAT)
$pat = ""

# Convert PAT to a Base64 string
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($pat)"))

# Set up the HTTP headers
$headers = @{
    Authorization = "Basic $base64AuthInfo"
}

# The name of your Azure DevOps organization
$organization = ""

# read the CSV with the IDs of the test cases
$csvData = Import-Csv -Path ".\DoubleTestCase1.28.0.csv"
 
# Retrieve the name of the first column
$firstColumnName = $csvData[0].PSObject.Properties.Name[0]
 
# Output the values of the first column
# $csvData | ForEach-Object { $_.$firstColumnName }

Foreach ($item in $csvData)
{
    $workItemId = $item.ID

    # Construct the REST API URL for deleting a work item
    $uri = "https://dev.azure.com/$organization/_apis/test/testcases/" + $workItemId + "?api-version=6.0"
    #Write-Output $uri

    # Send the DELETE request
    Invoke-RestMethod -Uri $uri -Method Delete -Headers $headers
}

Script to remove the parents from the other test cases

$pat = the personal access token created in step 5
$organization = the name of your organization and your project, e.g. company_x/project_y
$csvData = the path to the CSVfile that was created in step 10

# Your Personal Access Token (PAT)
$pat = ""

# Convert PAT to a Base64 string
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($pat)"))

# Set up the HTTP headers
$headers = @{
    Authorization = "Basic $base64AuthInfo"
}

# The name of your Azure DevOps organization
$organization = ""

# read the CSV with the IDs of the test cases
$csvData = Import-Csv -Path ".\TestCasesWithLinkToTestPlan1.28.0.csv"
 
# Retrieve the name of the first column
$firstColumnName = $csvData[0].PSObject.Properties.Name[0]
 
# Output the values of the first column
#$csvData | ForEach-Object { $_.$firstColumnName }

Foreach ($item in $csvData)
{
    $workItemId = $item.ID
    
    # Construct the REST API URL for getting the parent of a work item
    $uri = "https://dev.azure.com/$organization/_apis/wit/workitems/" + $workItemId + "?`$expand=Relations&api-version=6.0"
    $invRestMethParams = @{
    
        Uri = $uri
        Method = 'get'
        Headers= @{Authorization=("Basic {0}" -f $base64AuthInfo)}
      }
    $result = Invoke-RestMethod @invRestMethParams

    Foreach ($rel in $result.relations)
    {
        # get the current type of the link (we may only continue if the type of link is "Parent"
        $linkType = $rel.attributes.name
        
        if ($linkType -eq "Parent")
        {
            # get the index of the parent link, needed to build the body of the remove command
            $indexOfParent = $result.relations.IndexOf($rel)
            $body='[
             {
                "op": "remove",
                "path": "/relations/' + $indexOfParent + '",
                "value": null
              }
             ]'

            # build the URI string of the parent
            $uri2 = "https://dev.azure.com/$organization/_apis/wit/workitems/" + $workItemId + "?`$expand=Relations&api-version=6.0"
            
            # perfrom the actual remove action
            $invRestMethParams = @{
              Uri = $uri2
              Method = 'PATCH'
              ContentType = 'application/json-patch+json'
              Headers = @{Authorization=("Basic {0}" -f $base64AuthInfo)}
              Body = $body
            }
            Invoke-RestMethod @invRestMethParams
        }
    }
}



Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.