Skip to content

Commit d32805b

Browse files
authored
Merge pull request #13 from cmbrose/dev/bcaleb/record-id-encoding
Correctly handle url encoding ids
2 parents 7258166 + 02b6aa8 commit d32805b

7 files changed

+182
-26
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
# Release Notes
22
All notable changes and release history of the "cosmos-db" module will be documented in this file.
33

4+
## 1.14
5+
* 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 `'/'`
6+
7+
## 1.13
8+
* Consistent handling of request errors in PS7 vs. PS5
9+
* Gives a better error message if the database doesn't exist
10+
11+
## 1.10
12+
* Fixes a bug that caused 401s with ids that contained uppercase characters
13+
414
## 1.9
515
* Add support for pipelined objects in `Remove-CosmosDbRecord`
616
* Add optional `GetPartitionKeyBlock` argument to `Remove-CosmosDbRecord`

cosmos-db/cosmos-db.psd1

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# RootModule = ''
1212

1313
# Version number of this module.
14-
ModuleVersion = '1.13'
14+
ModuleVersion = '1.14'
1515

1616
# Supported PSEditions
1717
# CompatiblePSEditions = @()

cosmos-db/cosmos-db.psm1

+19-10
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,17 @@ Function Get-CollectionsUrl([string]$Container, [string]$Collection) {
2222
return "$DB_TYPE/$Container/$COLLS_TYPE/$Collection"
2323
}
2424

25+
# Returns an object with the properties ApiUrl and ResourceUrl.
26+
# - 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)
27+
# - ResourceUrl uses the raw RecordId and should be used in the auth header (e.g. Get-AuthorizationHeader)
2528
Function Get-DocumentsUrl([string]$Container, [string]$Collection, [string]$RecordId) {
26-
return (Get-CollectionsUrl $Container $Collection) + "/$DOCS_TYPE/$RecordId"
29+
$collectionsUrl = Get-CollectionsUrl $Container $Collection
30+
$encodedRecordId = [uri]::EscapeDataString($RecordId)
31+
32+
return @{
33+
ApiUrl = "$collectionsUrl/$DOCS_TYPE/$encodedRecordId";
34+
ResourceUrl = "$collectionsUrl/$DOCS_TYPE/$RecordId";
35+
}
2736
}
2837

2938
Function Get-Time() {
@@ -340,13 +349,13 @@ Function Get-CosmosDbRecord(
340349
[parameter(Mandatory = $false)][string]$PartitionKey = "") {
341350
begin {
342351
$baseUrl = Get-BaseDatabaseUrl $Database
343-
$documentUrl = Get-DocumentsUrl $Container $Collection $RecordId
352+
$documentUrls = Get-DocumentsUrl $Container $Collection $RecordId
344353

345-
$url = "$baseUrl/$documentUrl"
354+
$url = "$baseUrl/$($documentUrls.ApiUrl)"
346355

347356
$now = Get-Time
348357

349-
$encodedAuthString = Get-AuthorizationHeader -ResourceGroup $ResourceGroup -SubscriptionId $SubscriptionId -Database $Database -verb $GET_VERB -resourceType $DOCS_TYPE -resourceUrl $documentUrl -now $now
358+
$encodedAuthString = Get-AuthorizationHeader -ResourceGroup $ResourceGroup -SubscriptionId $SubscriptionId -Database $Database -verb $GET_VERB -resourceType $DOCS_TYPE -resourceUrl $documentUrls.ResourceUrl -now $now
350359

351360
$requestPartitionKey = if ($PartitionKey) { $PartitionKey } else { $RecordId }
352361
}
@@ -773,13 +782,13 @@ Function Update-CosmosDbRecord {
773782
}
774783
process {
775784
try {
776-
$documentUrl = Get-DocumentsUrl $Container $Collection $Object.id
785+
$documentUrls = Get-DocumentsUrl $Container $Collection $Object.id
777786

778-
$url = "$baseUrl/$documentUrl"
787+
$url = "$baseUrl/$($documentUrls.ApiUrl)"
779788

780789
$now = Get-Time
781790

782-
$encodedAuthString = Get-AuthorizationHeader -ResourceGroup $ResourceGroup -SubscriptionId $SubscriptionId -Database $Database -verb $PUT_VERB -resourceType $DOCS_TYPE -resourceUrl $documentUrl -now $now
791+
$encodedAuthString = Get-AuthorizationHeader -ResourceGroup $ResourceGroup -SubscriptionId $SubscriptionId -Database $Database -verb $PUT_VERB -resourceType $DOCS_TYPE -resourceUrl $documentUrls.ResourceUrl -now $now
783792

784793
$requestPartitionKey = if ($PartitionKey) { $PartitionKey } elseif ($GetPartitionKeyBlock) { Invoke-Command -ScriptBlock $GetPartitionKeyBlock -ArgumentList $Object } else { $Object.Id }
785794

@@ -866,13 +875,13 @@ Function Remove-CosmosDbRecord {
866875
try {
867876
$id = if ($RecordId) { $RecordId } else { $Object.id }
868877

869-
$documentUrl = Get-DocumentsUrl $Container $Collection $id
878+
$documentUrls = Get-DocumentsUrl $Container $Collection $id
870879

871-
$url = "$baseUrl/$documentUrl"
880+
$url = "$baseUrl/$($documentUrls.ApiUrl)"
872881

873882
$now = Get-Time
874883

875-
$encodedAuthString = Get-AuthorizationHeader -ResourceGroup $ResourceGroup -SubscriptionId $SubscriptionId -Database $Database -verb $DELETE_VERB -resourceType $DOCS_TYPE -resourceUrl $documentUrl -now $now
884+
$encodedAuthString = Get-AuthorizationHeader -ResourceGroup $ResourceGroup -SubscriptionId $SubscriptionId -Database $Database -verb $DELETE_VERB -resourceType $DOCS_TYPE -resourceUrl $documentUrls.ResourceUrl -now $now
876885

877886
$requestPartitionKey = if ($PartitionKey) { $PartitionKey } elseif ($GetPartitionKeyBlock) { Invoke-Command -ScriptBlock $GetPartitionKeyBlock -ArgumentList $Object } else { $id }
878887

tests/Get-CosmosDbRecord.Tests.ps1

+38-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Import-Module $PSScriptRoot\..\cosmos-db\cosmos-db.psm1 -Force
33

44
InModuleScope cosmos-db {
55
Describe "Get-CosmosDbRecord" {
6-
BeforeAll {
6+
BeforeEach {
77
Use-CosmosDbInternalFlag -EnableCaching $false
88

99
. $PSScriptRoot\Utils.ps1
@@ -19,8 +19,7 @@ InModuleScope cosmos-db {
1919

2020
$MOCK_AUTH_HEADER = "MockAuthHeader"
2121

22-
Function VerifyGetAuthHeader($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now)
23-
{
22+
Function VerifyGetAuthHeader($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now) {
2423
$ResourceGroup | Should -Be $MOCK_RG
2524
$SubscriptionId | Should -Be $MOCK_SUB
2625

@@ -29,10 +28,9 @@ InModuleScope cosmos-db {
2928
$resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$MOCK_RECORD_ID"
3029
}
3130

32-
Function VerifyInvokeCosmosDbApiRequest($verb, $url, $body, $headers, $partitionKey=$MOCK_RECORD_ID)
33-
{
31+
Function VerifyInvokeCosmosDbApiRequest($verb, $url, $body, $headers, $apiUriRecordId=$MOCK_RECORD_ID, $partitionKey=$MOCK_RECORD_ID) {
3432
$verb | Should -Be "get"
35-
$url | Should -Be "https://$MOCK_DB.documents.azure.com/dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$MOCK_RECORD_ID"
33+
$url | Should -Be "https://$MOCK_DB.documents.azure.com/dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$apiUriRecordId"
3634
$body | Should -Be $null
3735

3836
$global:capturedNow | Should -Not -Be $null
@@ -83,7 +81,7 @@ InModuleScope cosmos-db {
8381
Mock Invoke-CosmosDbApiRequest {
8482
param($verb, $url, $body, $headers)
8583

86-
VerifyInvokeCosmosDbApiRequest $verb $url $body $headers $partitionKey | Out-Null
84+
VerifyInvokeCosmosDbApiRequest $verb $url $body $headers -partitionKey $partitionKey | Out-Null
8785

8886
$response
8987
}
@@ -93,6 +91,39 @@ InModuleScope cosmos-db {
9391
$result | Should -BeExactly $response
9492
}
9593

94+
It "Url encodes the record id in the API url" {
95+
$response = @{
96+
StatusCode = 200;
97+
Content = "{}"
98+
}
99+
100+
$testRecordId = "MOCK/RECORD/ID"
101+
$expectedApiRecordId = [uri]::EscapeDataString($testRecordId)
102+
$expectedAuthHeaderRecordId = $testRecordId # The id in the auth header should not be encoded
103+
104+
Mock Invoke-CosmosDbApiRequest {
105+
param($verb, $url, $body, $headers)
106+
107+
VerifyInvokeCosmosDbApiRequest $verb $url $body $headers -apiUriRecordId $expectedApiRecordId -partitionKey $testRecordId | Out-Null
108+
109+
$response
110+
}
111+
112+
Mock Get-AuthorizationHeader {
113+
param($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now)
114+
115+
$resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$expectedAuthHeaderRecordId"
116+
117+
$global:capturedNow = $now
118+
119+
$MOCK_AUTH_HEADER
120+
}
121+
122+
$result = Get-CosmosDbRecord -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION -RecordId $testRecordId
123+
124+
$result | Should -BeExactly $response
125+
}
126+
96127
It "Should handle exceptions gracefully" {
97128
$response = [System.Net.HttpWebResponse]@{}
98129

tests/Get-PartitionKeyRangesOrError.Tests.ps1

-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ InModuleScope cosmos-db {
103103
$response
104104
}
105105

106-
Write-host "1"
107106
$_ = Get-PartitionKeyRangesOrError -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION
108107

109108
$urlKey = "https://$MOCK_DB.documents.azure.com/dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/pkranges"

tests/Remove-CosmosDbRecord.Tests.ps1

+76-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Import-Module $PSScriptRoot\..\cosmos-db\cosmos-db.psm1 -Force
33

44
InModuleScope cosmos-db {
55
Describe "Remove-CosmosDbRecord" {
6-
BeforeAll {
6+
BeforeEach {
77
Use-CosmosDbInternalFlag -EnableCaching $false
88

99
. $PSScriptRoot\Utils.ps1
@@ -28,9 +28,9 @@ InModuleScope cosmos-db {
2828
$resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$MOCK_RECORD_ID"
2929
}
3030

31-
Function VerifyInvokeCosmosDbApiRequest($verb, $url, $body, $headers, $partitionKey = $MOCK_RECORD_ID) {
31+
Function VerifyInvokeCosmosDbApiRequest($verb, $url, $body, $headers, $apiUriRecordId=$MOCK_RECORD_ID, $partitionKey=$MOCK_RECORD_ID) {
3232
$verb | Should -Be "delete"
33-
$url | Should -Be "https://$MOCK_DB.documents.azure.com/dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$MOCK_RECORD_ID"
33+
$url | Should -Be "https://$MOCK_DB.documents.azure.com/dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$apiUriRecordId"
3434
$body | Should -Be $null
3535

3636
$global:capturedNow | Should -Not -Be $null
@@ -81,7 +81,7 @@ InModuleScope cosmos-db {
8181
Mock Invoke-CosmosDbApiRequest {
8282
param($verb, $url, $body, $headers)
8383

84-
VerifyInvokeCosmosDbApiRequest $verb $url $body $headers $partitionKey | Out-Null
84+
VerifyInvokeCosmosDbApiRequest $verb $url $body $headers -partitionKey $partitionKey | Out-Null
8585

8686
$response
8787
}
@@ -125,7 +125,7 @@ InModuleScope cosmos-db {
125125
Mock Invoke-CosmosDbApiRequest {
126126
param($verb, $url, $body, $headers)
127127

128-
VerifyInvokeCosmosDbApiRequest $verb $url $body $headers $partitionKey | Out-Null
128+
VerifyInvokeCosmosDbApiRequest $verb $url $body $headers -partitionKey $partitionKey | Out-Null
129129

130130
$response
131131
}
@@ -150,7 +150,7 @@ InModuleScope cosmos-db {
150150
Mock Invoke-CosmosDbApiRequest {
151151
param($verb, $url, $body, $headers)
152152

153-
VerifyInvokeCosmosDbApiRequest $verb $url $body $headers $partitionKey | Out-Null
153+
VerifyInvokeCosmosDbApiRequest $verb $url $body $headers -partitionKey $partitionKey | Out-Null
154154

155155
$response
156156
}
@@ -169,6 +169,76 @@ InModuleScope cosmos-db {
169169
$result | Should -BeExactly $response
170170
}
171171

172+
It "Url encodes the record id in the API url" {
173+
$response = @{
174+
StatusCode = 200;
175+
Content = "{}"
176+
}
177+
178+
$testRecordId = "MOCK/RECORD/ID"
179+
$expectedApiRecordId = [uri]::EscapeDataString($testRecordId)
180+
$expectedAuthHeaderRecordId = $testRecordId # The id in the auth header should not be encoded
181+
182+
Mock Invoke-CosmosDbApiRequest {
183+
param($verb, $url, $body, $headers)
184+
185+
VerifyInvokeCosmosDbApiRequest $verb $url $body $headers -apiUriRecordId $expectedApiRecordId -partitionKey $testRecordId | Out-Null
186+
187+
$response
188+
}
189+
190+
Mock Get-AuthorizationHeader {
191+
param($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now)
192+
193+
$resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$expectedAuthHeaderRecordId"
194+
195+
$global:capturedNow = $now
196+
197+
$MOCK_AUTH_HEADER
198+
}
199+
200+
$result = Remove-CosmosDbRecord -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION -RecordId $testRecordId
201+
202+
$result | Should -BeExactly $response
203+
}
204+
205+
It "Url encodes the record id in the API url from an input object" {
206+
$response = @{
207+
StatusCode = 200;
208+
Content = "{}"
209+
}
210+
211+
$testRecordId = "MOCK/RECORD/ID"
212+
$expectedApiRecordId = [uri]::EscapeDataString($testRecordId)
213+
$expectedAuthHeaderRecordId = $testRecordId # The id in the auth header should not be encoded
214+
215+
Mock Invoke-CosmosDbApiRequest {
216+
param($verb, $url, $body, $headers)
217+
218+
VerifyInvokeCosmosDbApiRequest $verb $url $body $headers -apiUriRecordId $expectedApiRecordId -partitionKey $testRecordId | Out-Null
219+
220+
$response
221+
}
222+
223+
Mock Get-AuthorizationHeader {
224+
param($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now)
225+
226+
$resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$expectedAuthHeaderRecordId"
227+
228+
$global:capturedNow = $now
229+
230+
$MOCK_AUTH_HEADER
231+
}
232+
233+
$obj = @{
234+
id = $testRecordId
235+
}
236+
237+
$result = $obj | Remove-CosmosDbRecord -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION
238+
239+
$result | Should -BeExactly $response
240+
}
241+
172242
It "Should handle exceptions gracefully" {
173243
$response = [System.Net.HttpWebResponse]@{}
174244

tests/Update-CosmosDbRecord.Tests.ps1

+38-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Import-Module $PSScriptRoot\..\cosmos-db\cosmos-db.psm1 -Force
33

44
InModuleScope cosmos-db {
55
Describe "Update-CosmosDbRecord" {
6-
BeforeAll {
6+
BeforeEach {
77
Use-CosmosDbInternalFlag -EnableCaching $false
88

99
. $PSScriptRoot\Utils.ps1
@@ -200,6 +200,43 @@ InModuleScope cosmos-db {
200200

201201
Assert-MockCalled Invoke-CosmosDbApiRequest -Times $payloads.Count
202202
}
203+
204+
It "Url encodes the record id in the API url" {
205+
$response = @{
206+
StatusCode = 200;
207+
Content = "{}"
208+
}
209+
210+
$testRecordId = "MOCK/RECORD/ID"
211+
$expectedApiRecordId = [uri]::EscapeDataString($testRecordId)
212+
$expectedAuthHeaderRecordId = $testRecordId # The id in the auth header should not be encoded
213+
214+
$payload = @{
215+
id = $testRecordId
216+
}
217+
218+
Mock Invoke-CosmosDbApiRequest {
219+
param($verb, $url, $body, $headers)
220+
221+
VerifyInvokeCosmosDbApiRequest $verb $url $body $payload $headers -expectedId $expectedApiRecordId -expectedPartitionKey $testRecordId | Out-Null
222+
223+
$response
224+
}
225+
226+
Mock Get-AuthorizationHeader {
227+
param($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now)
228+
229+
$resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$expectedAuthHeaderRecordId"
230+
231+
$global:capturedNow = $now
232+
233+
$MOCK_AUTH_HEADER
234+
}
235+
236+
$result = $payload | Update-CosmosDbRecord -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION
237+
238+
$result | Should -BeExactly $response
239+
}
203240

204241
It "Should handle exceptions gracefully" {
205242
$response = [System.Net.HttpWebResponse]@{}

0 commit comments

Comments
 (0)