Skip to content

Commit a079871

Browse files
authored
Add etag verfication in update calls (#14)
1 parent d32805b commit a079871

File tree

2 files changed

+74
-10
lines changed

2 files changed

+74
-10
lines changed

cosmos-db/cosmos-db.psm1

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ Function Get-AuthorizationHeader([string]$ResourceGroup, [string]$SubscriptionId
123123
Get-EncodedAuthString -signatureHash $signatureHash
124124
}
125125

126-
Function Get-CommonHeaders([string]$now, [string]$encodedAuthString, [string]$contentType = "application/json", [bool]$isQuery = $false, [string]$PartitionKey = $null) {
126+
Function Get-CommonHeaders([string]$now, [string]$encodedAuthString, [string]$contentType = "application/json", [bool]$isQuery = $false, [string]$PartitionKey = $null, [string]$Etag = $null) {
127127
$headers = @{
128128
"x-ms-date" = $now;
129129
"x-ms-version" = $API_VERSION;
@@ -139,6 +139,10 @@ Function Get-CommonHeaders([string]$now, [string]$encodedAuthString, [string]$co
139139
if ($PartitionKey) {
140140
$headers["x-ms-documentdb-partitionkey"] = "[`"$PartitionKey`"]"
141141
}
142+
143+
if ($Etag) {
144+
$headers["If-Match"] = $Etag
145+
}
142146

143147
$headers
144148
}
@@ -738,6 +742,8 @@ Function New-CosmosDbRecord {
738742
[Optional] The record's partition key. Default is the `id` property of `Object`. Required if using a custom partition strategy.
739743
.PARAMETER GetPartitionKeyBlock
740744
[Optional] Callback to get the partition key from the input object. Default is the `id` property of `Object`. Required if using a custom partition strategy.
745+
.PARAMETER EnforceOptimisticConcurrency
746+
[Optional] Boolean specifying whether to enforce optimistic concurrency. Default is $true.
741747
742748
.EXAMPLE
743749
$> Update-CosmosDbRecord -Object @{ id = 1234; key = value } ...
@@ -774,7 +780,9 @@ Function Update-CosmosDbRecord {
774780
[parameter(Mandatory = $true)][string]$Collection,
775781
[parameter(Mandatory = $false)][string]$SubscriptionId = "",
776782
[parameter(Mandatory = $false, ParameterSetName = "ExplicitPartitionKey")][string]$PartitionKey = "",
777-
[parameter(Mandatory = $false, ParameterSetName = "ParttionKeyCallback")]$GetPartitionKeyBlock = $null
783+
[parameter(Mandatory = $false, ParameterSetName = "ParttionKeyCallback")]$GetPartitionKeyBlock = $null,
784+
[parameter(Mandatory = $false)][bool]$EnforceOptimisticConcurrency = $true
785+
778786
)
779787

780788
begin {
@@ -792,7 +800,11 @@ Function Update-CosmosDbRecord {
792800

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

795-
$headers = Get-CommonHeaders -now $now -encodedAuthString $encodedAuthString -PartitionKey $requestPartitionKey
803+
if ($EnforceOptimisticConcurrency) {
804+
$headers = Get-CommonHeaders -now $now -encodedAuthString $encodedAuthString -PartitionKey $requestPartitionKey -Etag $Object._etag
805+
} else {
806+
$headers = Get-CommonHeaders -now $now -encodedAuthString $encodedAuthString -PartitionKey $requestPartitionKey
807+
}
796808

797809
Invoke-CosmosDbApiRequest -Verb $PUT_VERB -Url $url -Body $Object -Headers $headers
798810
}

tests/Update-CosmosDbRecord.Tests.ps1

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ InModuleScope cosmos-db {
1616
$MOCK_CONTAINER = "MOCK_CONTAINER"
1717
$MOCK_COLLECTION = "MOCK_COLLECTION"
1818
$MOCK_RECORD_ID = "MOCK_RECORD_ID"
19+
$MOCK_ETAG = "MOCK_ETAG"
1920

2021
$MOCK_AUTH_HEADER = "MockAuthHeader"
2122

@@ -29,7 +30,7 @@ InModuleScope cosmos-db {
2930
$resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$expectedId"
3031
}
3132

32-
Function VerifyInvokeCosmosDbApiRequest($verb, $url, $actualBody, $expectedBody, $headers, $expectedId=$MOCK_RECORD_ID, $expectedPartitionKey=$null)
33+
Function VerifyInvokeCosmosDbApiRequest($verb, $url, $actualBody, $expectedBody, $headers, $expectedId=$MOCK_RECORD_ID, $expectedPartitionKey=$null, $enforceOptimisticConcurrency=$true)
3334
{
3435
$verb | Should -Be "put"
3536
$url | Should -Be "https://$MOCK_DB.documents.azure.com/dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$expectedId"
@@ -39,8 +40,12 @@ InModuleScope cosmos-db {
3940
$global:capturedNow | Should -Not -Be $null
4041

4142
$expectedPartitionKey = if ($expectedPartitionKey) { $expectedPartitionKey } else { $expectedId }
42-
$expectedHeaders = Get-CommonHeaders -now $global:capturedNow -encodedAuthString $MOCK_AUTH_HEADER -PartitionKey $expectedPartitionKey
43-
43+
44+
if ($EnforceOptimisticConcurrency) {
45+
$expectedHeaders = $expectedHeaders = Get-CommonHeaders -now $global:capturedNow -encodedAuthString $MOCK_AUTH_HEADER -PartitionKey $expectedPartitionKey -Etag $MOCK_ETAG
46+
} else {
47+
$expectedHeaders = Get-CommonHeaders -now $global:capturedNow -encodedAuthString $MOCK_AUTH_HEADER -PartitionKey $expectedPartitionKey
48+
}
4449
AssertHashtablesEqual $expectedHeaders $headers
4550
}
4651

@@ -65,6 +70,7 @@ InModuleScope cosmos-db {
6570
id = $MOCK_RECORD_ID;
6671
key1 = "value1";
6772
key2 = 2;
73+
"_etag" = $MOCK_ETAG;
6874
}
6975

7076
Mock Invoke-CosmosDbApiRequest {
@@ -92,6 +98,7 @@ InModuleScope cosmos-db {
9298
id = $MOCK_RECORD_ID;
9399
key1 = "value1";
94100
key2 = 2;
101+
"_etag" = $MOCK_ETAG;
95102
}
96103

97104
$MOCK_PARTITION_KEY = "MOCK_PARTITION_KEY"
@@ -121,6 +128,7 @@ InModuleScope cosmos-db {
121128
id = $MOCK_RECORD_ID;
122129
key1 = "value1";
123130
key2 = 2;
131+
"_etag" = $MOCK_ETAG;
124132
}
125133

126134
$MOCK_PARTITION_KEY = "MOCK_PARTITION_KEY"
@@ -147,11 +155,48 @@ InModuleScope cosmos-db {
147155
Assert-MockCalled Invoke-CosmosDbApiRequest -Times 1
148156
}
149157

158+
It "Optimistic concurrency can be disabled" {
159+
$response = @{
160+
StatusCode = 200;
161+
Content = "{}"
162+
}
163+
164+
$payload = @{
165+
id = $MOCK_RECORD_ID;
166+
key1 = "value1";
167+
key2 = 2;
168+
"_etag" = $MOCK_ETAG;
169+
}
170+
171+
$MOCK_PARTITION_KEY = "MOCK_PARTITION_KEY"
172+
$MOCK_GET_PARTITION_KEY = {
173+
param($obj)
174+
175+
$obj | Should -BeExactly $payload | Out-Null
176+
177+
$MOCK_PARTITION_KEY
178+
}
179+
180+
Mock Invoke-CosmosDbApiRequest {
181+
param($verb, $url, $body, $headers)
182+
183+
VerifyInvokeCosmosDbApiRequest $verb $url $body $payload $headers -ExpectedPartitionKey $MOCK_PARTITION_KEY -EnforceOptimisticConcurrency $false | Out-Null
184+
185+
$response
186+
}
187+
188+
$result = $payload | Update-CosmosDbRecord -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION -GetPartitionKeyBlock $MOCK_GET_PARTITION_KEY -EnforceOptimisticConcurrency $false
189+
190+
$result | Should -BeExactly $response
191+
192+
Assert-MockCalled Invoke-CosmosDbApiRequest -Times 1
193+
}
194+
150195
It "Sends correct request with custom partition key callback for multiple inputs" {
151196
$payloads = @(
152-
@{ id = "1" };
153-
@{ id = "2" };
154-
@{ id = "3" };
197+
@{ id = "1"; "_etag" = $MOCK_ETAG };
198+
@{ id = "2"; "_etag" = $MOCK_ETAG };
199+
@{ id = "3"; "_etag" = $MOCK_ETAG };
155200
)
156201

157202
$global:idx = 0
@@ -199,6 +244,7 @@ InModuleScope cosmos-db {
199244
$result.Count | Should -Be $payloads.Count
200245

201246
Assert-MockCalled Invoke-CosmosDbApiRequest -Times $payloads.Count
247+
Assert-MockCalled Get-AuthorizationHeader -Times $payloads.Count
202248
}
203249

204250
It "Url encodes the record id in the API url" {
@@ -212,7 +258,8 @@ InModuleScope cosmos-db {
212258
$expectedAuthHeaderRecordId = $testRecordId # The id in the auth header should not be encoded
213259

214260
$payload = @{
215-
id = $testRecordId
261+
id = $testRecordId;
262+
"_etag" = $MOCK_ETAG;
216263
}
217264

218265
Mock Invoke-CosmosDbApiRequest {
@@ -236,6 +283,9 @@ InModuleScope cosmos-db {
236283
$result = $payload | Update-CosmosDbRecord -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION
237284

238285
$result | Should -BeExactly $response
286+
287+
Assert-MockCalled Invoke-CosmosDbApiRequest -Times 1
288+
Assert-MockCalled Get-AuthorizationHeader -Times 1
239289
}
240290

241291
It "Should handle exceptions gracefully" {
@@ -247,6 +297,7 @@ InModuleScope cosmos-db {
247297
id = $MOCK_RECORD_ID;
248298
key1 = "value1";
249299
key2 = 2;
300+
"_etag" = $MOCK_ETAG;
250301
}
251302

252303
Mock Invoke-CosmosDbApiRequest {
@@ -269,6 +320,7 @@ InModuleScope cosmos-db {
269320

270321
$result | Should -BeExactly $recordResponse
271322
Assert-MockCalled Get-ExceptionResponseOrThrow -Times 1
323+
Assert-MockCalled Invoke-CosmosDbApiRequest -Times 1
272324
}
273325
}
274326
}

0 commit comments

Comments
 (0)