Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`
Expand Down
2 changes: 1 addition & 1 deletion cosmos-db/cosmos-db.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# RootModule = ''

# Version number of this module.
ModuleVersion = '1.13'
ModuleVersion = '1.14'

# Supported PSEditions
# CompatiblePSEditions = @()
Expand Down
29 changes: 19 additions & 10 deletions cosmos-db/cosmos-db.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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 }
}
Expand Down Expand Up @@ -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 }

Expand Down Expand Up @@ -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 }

Expand Down
45 changes: 38 additions & 7 deletions tests/Get-CosmosDbRecord.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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]@{}

Expand Down
1 change: 0 additions & 1 deletion tests/Get-PartitionKeyRangesOrError.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
82 changes: 76 additions & 6 deletions tests/Remove-CosmosDbRecord.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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]@{}

Expand Down
39 changes: 38 additions & 1 deletion tests/Update-CosmosDbRecord.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]@{}
Expand Down