diff --git a/CHANGELOG.md b/CHANGELOG.md index 69a9738..73f95d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Release Notes All notable changes and release history of the "cosmos-db" module will be documented in this file. +## 1.14 +* Fixes a bug where record ids were not encoded in the API calls which broke Get-, Remove-, and Update- for records with ids that contained characters such as `'/'` + +## 1.13 +* Consistent handling of request errors in PS7 vs. PS5 +* Gives a better error message if the database doesn't exist + +## 1.10 +* Fixes a bug that caused 401s with ids that contained uppercase characters + ## 1.9 * Add support for pipelined objects in `Remove-CosmosDbRecord` * Add optional `GetPartitionKeyBlock` argument to `Remove-CosmosDbRecord` diff --git a/cosmos-db/cosmos-db.psd1 b/cosmos-db/cosmos-db.psd1 index 7e47a3d..895d2b9 100644 --- a/cosmos-db/cosmos-db.psd1 +++ b/cosmos-db/cosmos-db.psd1 @@ -11,7 +11,7 @@ # RootModule = '' # Version number of this module. - ModuleVersion = '1.13' + ModuleVersion = '1.14' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/cosmos-db/cosmos-db.psm1 b/cosmos-db/cosmos-db.psm1 index 64996ca..81f08a2 100644 --- a/cosmos-db/cosmos-db.psm1 +++ b/cosmos-db/cosmos-db.psm1 @@ -22,8 +22,17 @@ Function Get-CollectionsUrl([string]$Container, [string]$Collection) { return "$DB_TYPE/$Container/$COLLS_TYPE/$Collection" } +# Returns an object with the properties ApiUrl and ResourceUrl. +# - ApiUrl uses an escaped version of the RecordId to support having characters such as '/' in the record Id and should be used as the API url (e.g. Invoke-WebRequest) +# - ResourceUrl uses the raw RecordId and should be used in the auth header (e.g. Get-AuthorizationHeader) Function Get-DocumentsUrl([string]$Container, [string]$Collection, [string]$RecordId) { - return (Get-CollectionsUrl $Container $Collection) + "/$DOCS_TYPE/$RecordId" + $collectionsUrl = Get-CollectionsUrl $Container $Collection + $encodedRecordId = [uri]::EscapeDataString($RecordId) + + return @{ + ApiUrl = "$collectionsUrl/$DOCS_TYPE/$encodedRecordId"; + ResourceUrl = "$collectionsUrl/$DOCS_TYPE/$RecordId"; + } } Function Get-Time() { @@ -340,13 +349,13 @@ Function Get-CosmosDbRecord( [parameter(Mandatory = $false)][string]$PartitionKey = "") { begin { $baseUrl = Get-BaseDatabaseUrl $Database - $documentUrl = Get-DocumentsUrl $Container $Collection $RecordId + $documentUrls = Get-DocumentsUrl $Container $Collection $RecordId - $url = "$baseUrl/$documentUrl" + $url = "$baseUrl/$($documentUrls.ApiUrl)" $now = Get-Time - $encodedAuthString = Get-AuthorizationHeader -ResourceGroup $ResourceGroup -SubscriptionId $SubscriptionId -Database $Database -verb $GET_VERB -resourceType $DOCS_TYPE -resourceUrl $documentUrl -now $now + $encodedAuthString = Get-AuthorizationHeader -ResourceGroup $ResourceGroup -SubscriptionId $SubscriptionId -Database $Database -verb $GET_VERB -resourceType $DOCS_TYPE -resourceUrl $documentUrls.ResourceUrl -now $now $requestPartitionKey = if ($PartitionKey) { $PartitionKey } else { $RecordId } } @@ -773,13 +782,13 @@ Function Update-CosmosDbRecord { } process { try { - $documentUrl = Get-DocumentsUrl $Container $Collection $Object.id + $documentUrls = Get-DocumentsUrl $Container $Collection $Object.id - $url = "$baseUrl/$documentUrl" + $url = "$baseUrl/$($documentUrls.ApiUrl)" $now = Get-Time - $encodedAuthString = Get-AuthorizationHeader -ResourceGroup $ResourceGroup -SubscriptionId $SubscriptionId -Database $Database -verb $PUT_VERB -resourceType $DOCS_TYPE -resourceUrl $documentUrl -now $now + $encodedAuthString = Get-AuthorizationHeader -ResourceGroup $ResourceGroup -SubscriptionId $SubscriptionId -Database $Database -verb $PUT_VERB -resourceType $DOCS_TYPE -resourceUrl $documentUrls.ResourceUrl -now $now $requestPartitionKey = if ($PartitionKey) { $PartitionKey } elseif ($GetPartitionKeyBlock) { Invoke-Command -ScriptBlock $GetPartitionKeyBlock -ArgumentList $Object } else { $Object.Id } @@ -866,13 +875,13 @@ Function Remove-CosmosDbRecord { try { $id = if ($RecordId) { $RecordId } else { $Object.id } - $documentUrl = Get-DocumentsUrl $Container $Collection $id + $documentUrls = Get-DocumentsUrl $Container $Collection $id - $url = "$baseUrl/$documentUrl" + $url = "$baseUrl/$($documentUrls.ApiUrl)" $now = Get-Time - $encodedAuthString = Get-AuthorizationHeader -ResourceGroup $ResourceGroup -SubscriptionId $SubscriptionId -Database $Database -verb $DELETE_VERB -resourceType $DOCS_TYPE -resourceUrl $documentUrl -now $now + $encodedAuthString = Get-AuthorizationHeader -ResourceGroup $ResourceGroup -SubscriptionId $SubscriptionId -Database $Database -verb $DELETE_VERB -resourceType $DOCS_TYPE -resourceUrl $documentUrls.ResourceUrl -now $now $requestPartitionKey = if ($PartitionKey) { $PartitionKey } elseif ($GetPartitionKeyBlock) { Invoke-Command -ScriptBlock $GetPartitionKeyBlock -ArgumentList $Object } else { $id } diff --git a/tests/Get-CosmosDbRecord.Tests.ps1 b/tests/Get-CosmosDbRecord.Tests.ps1 index e9209cf..74a1f90 100644 --- a/tests/Get-CosmosDbRecord.Tests.ps1 +++ b/tests/Get-CosmosDbRecord.Tests.ps1 @@ -3,7 +3,7 @@ Import-Module $PSScriptRoot\..\cosmos-db\cosmos-db.psm1 -Force InModuleScope cosmos-db { Describe "Get-CosmosDbRecord" { - BeforeAll { + BeforeEach { Use-CosmosDbInternalFlag -EnableCaching $false . $PSScriptRoot\Utils.ps1 @@ -19,8 +19,7 @@ InModuleScope cosmos-db { $MOCK_AUTH_HEADER = "MockAuthHeader" - Function VerifyGetAuthHeader($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now) - { + Function VerifyGetAuthHeader($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now) { $ResourceGroup | Should -Be $MOCK_RG $SubscriptionId | Should -Be $MOCK_SUB @@ -29,10 +28,9 @@ InModuleScope cosmos-db { $resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$MOCK_RECORD_ID" } - Function VerifyInvokeCosmosDbApiRequest($verb, $url, $body, $headers, $partitionKey=$MOCK_RECORD_ID) - { + Function VerifyInvokeCosmosDbApiRequest($verb, $url, $body, $headers, $apiUriRecordId=$MOCK_RECORD_ID, $partitionKey=$MOCK_RECORD_ID) { $verb | Should -Be "get" - $url | Should -Be "https://$MOCK_DB.documents.azure.com/dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$MOCK_RECORD_ID" + $url | Should -Be "https://$MOCK_DB.documents.azure.com/dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$apiUriRecordId" $body | Should -Be $null $global:capturedNow | Should -Not -Be $null @@ -83,7 +81,7 @@ InModuleScope cosmos-db { Mock Invoke-CosmosDbApiRequest { param($verb, $url, $body, $headers) - VerifyInvokeCosmosDbApiRequest $verb $url $body $headers $partitionKey | Out-Null + VerifyInvokeCosmosDbApiRequest $verb $url $body $headers -partitionKey $partitionKey | Out-Null $response } @@ -93,6 +91,39 @@ InModuleScope cosmos-db { $result | Should -BeExactly $response } + It "Url encodes the record id in the API url" { + $response = @{ + StatusCode = 200; + Content = "{}" + } + + $testRecordId = "MOCK/RECORD/ID" + $expectedApiRecordId = [uri]::EscapeDataString($testRecordId) + $expectedAuthHeaderRecordId = $testRecordId # The id in the auth header should not be encoded + + Mock Invoke-CosmosDbApiRequest { + param($verb, $url, $body, $headers) + + VerifyInvokeCosmosDbApiRequest $verb $url $body $headers -apiUriRecordId $expectedApiRecordId -partitionKey $testRecordId | Out-Null + + $response + } + + Mock Get-AuthorizationHeader { + param($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now) + + $resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$expectedAuthHeaderRecordId" + + $global:capturedNow = $now + + $MOCK_AUTH_HEADER + } + + $result = Get-CosmosDbRecord -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION -RecordId $testRecordId + + $result | Should -BeExactly $response + } + It "Should handle exceptions gracefully" { $response = [System.Net.HttpWebResponse]@{} diff --git a/tests/Get-PartitionKeyRangesOrError.Tests.ps1 b/tests/Get-PartitionKeyRangesOrError.Tests.ps1 index dfc4fb1..f835fb1 100644 --- a/tests/Get-PartitionKeyRangesOrError.Tests.ps1 +++ b/tests/Get-PartitionKeyRangesOrError.Tests.ps1 @@ -103,7 +103,6 @@ InModuleScope cosmos-db { $response } - Write-host "1" $_ = Get-PartitionKeyRangesOrError -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION $urlKey = "https://$MOCK_DB.documents.azure.com/dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/pkranges" diff --git a/tests/Remove-CosmosDbRecord.Tests.ps1 b/tests/Remove-CosmosDbRecord.Tests.ps1 index 29cda00..8ce4617 100644 --- a/tests/Remove-CosmosDbRecord.Tests.ps1 +++ b/tests/Remove-CosmosDbRecord.Tests.ps1 @@ -3,7 +3,7 @@ Import-Module $PSScriptRoot\..\cosmos-db\cosmos-db.psm1 -Force InModuleScope cosmos-db { Describe "Remove-CosmosDbRecord" { - BeforeAll { + BeforeEach { Use-CosmosDbInternalFlag -EnableCaching $false . $PSScriptRoot\Utils.ps1 @@ -28,9 +28,9 @@ InModuleScope cosmos-db { $resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$MOCK_RECORD_ID" } - Function VerifyInvokeCosmosDbApiRequest($verb, $url, $body, $headers, $partitionKey = $MOCK_RECORD_ID) { + Function VerifyInvokeCosmosDbApiRequest($verb, $url, $body, $headers, $apiUriRecordId=$MOCK_RECORD_ID, $partitionKey=$MOCK_RECORD_ID) { $verb | Should -Be "delete" - $url | Should -Be "https://$MOCK_DB.documents.azure.com/dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$MOCK_RECORD_ID" + $url | Should -Be "https://$MOCK_DB.documents.azure.com/dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$apiUriRecordId" $body | Should -Be $null $global:capturedNow | Should -Not -Be $null @@ -81,7 +81,7 @@ InModuleScope cosmos-db { Mock Invoke-CosmosDbApiRequest { param($verb, $url, $body, $headers) - VerifyInvokeCosmosDbApiRequest $verb $url $body $headers $partitionKey | Out-Null + VerifyInvokeCosmosDbApiRequest $verb $url $body $headers -partitionKey $partitionKey | Out-Null $response } @@ -125,7 +125,7 @@ InModuleScope cosmos-db { Mock Invoke-CosmosDbApiRequest { param($verb, $url, $body, $headers) - VerifyInvokeCosmosDbApiRequest $verb $url $body $headers $partitionKey | Out-Null + VerifyInvokeCosmosDbApiRequest $verb $url $body $headers -partitionKey $partitionKey | Out-Null $response } @@ -150,7 +150,7 @@ InModuleScope cosmos-db { Mock Invoke-CosmosDbApiRequest { param($verb, $url, $body, $headers) - VerifyInvokeCosmosDbApiRequest $verb $url $body $headers $partitionKey | Out-Null + VerifyInvokeCosmosDbApiRequest $verb $url $body $headers -partitionKey $partitionKey | Out-Null $response } @@ -169,6 +169,76 @@ InModuleScope cosmos-db { $result | Should -BeExactly $response } + It "Url encodes the record id in the API url" { + $response = @{ + StatusCode = 200; + Content = "{}" + } + + $testRecordId = "MOCK/RECORD/ID" + $expectedApiRecordId = [uri]::EscapeDataString($testRecordId) + $expectedAuthHeaderRecordId = $testRecordId # The id in the auth header should not be encoded + + Mock Invoke-CosmosDbApiRequest { + param($verb, $url, $body, $headers) + + VerifyInvokeCosmosDbApiRequest $verb $url $body $headers -apiUriRecordId $expectedApiRecordId -partitionKey $testRecordId | Out-Null + + $response + } + + Mock Get-AuthorizationHeader { + param($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now) + + $resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$expectedAuthHeaderRecordId" + + $global:capturedNow = $now + + $MOCK_AUTH_HEADER + } + + $result = Remove-CosmosDbRecord -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION -RecordId $testRecordId + + $result | Should -BeExactly $response + } + + It "Url encodes the record id in the API url from an input object" { + $response = @{ + StatusCode = 200; + Content = "{}" + } + + $testRecordId = "MOCK/RECORD/ID" + $expectedApiRecordId = [uri]::EscapeDataString($testRecordId) + $expectedAuthHeaderRecordId = $testRecordId # The id in the auth header should not be encoded + + Mock Invoke-CosmosDbApiRequest { + param($verb, $url, $body, $headers) + + VerifyInvokeCosmosDbApiRequest $verb $url $body $headers -apiUriRecordId $expectedApiRecordId -partitionKey $testRecordId | Out-Null + + $response + } + + Mock Get-AuthorizationHeader { + param($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now) + + $resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$expectedAuthHeaderRecordId" + + $global:capturedNow = $now + + $MOCK_AUTH_HEADER + } + + $obj = @{ + id = $testRecordId + } + + $result = $obj | Remove-CosmosDbRecord -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION + + $result | Should -BeExactly $response + } + It "Should handle exceptions gracefully" { $response = [System.Net.HttpWebResponse]@{} diff --git a/tests/Update-CosmosDbRecord.Tests.ps1 b/tests/Update-CosmosDbRecord.Tests.ps1 index 4de8acf..003551a 100644 --- a/tests/Update-CosmosDbRecord.Tests.ps1 +++ b/tests/Update-CosmosDbRecord.Tests.ps1 @@ -3,7 +3,7 @@ Import-Module $PSScriptRoot\..\cosmos-db\cosmos-db.psm1 -Force InModuleScope cosmos-db { Describe "Update-CosmosDbRecord" { - BeforeAll { + BeforeEach { Use-CosmosDbInternalFlag -EnableCaching $false . $PSScriptRoot\Utils.ps1 @@ -200,6 +200,43 @@ InModuleScope cosmos-db { Assert-MockCalled Invoke-CosmosDbApiRequest -Times $payloads.Count } + + It "Url encodes the record id in the API url" { + $response = @{ + StatusCode = 200; + Content = "{}" + } + + $testRecordId = "MOCK/RECORD/ID" + $expectedApiRecordId = [uri]::EscapeDataString($testRecordId) + $expectedAuthHeaderRecordId = $testRecordId # The id in the auth header should not be encoded + + $payload = @{ + id = $testRecordId + } + + Mock Invoke-CosmosDbApiRequest { + param($verb, $url, $body, $headers) + + VerifyInvokeCosmosDbApiRequest $verb $url $body $payload $headers -expectedId $expectedApiRecordId -expectedPartitionKey $testRecordId | Out-Null + + $response + } + + Mock Get-AuthorizationHeader { + param($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now) + + $resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$expectedAuthHeaderRecordId" + + $global:capturedNow = $now + + $MOCK_AUTH_HEADER + } + + $result = $payload | Update-CosmosDbRecord -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION + + $result | Should -BeExactly $response + } It "Should handle exceptions gracefully" { $response = [System.Net.HttpWebResponse]@{}