From e4699d0ea325518046bfa3bfee6ab8f3d028080e Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Mon, 12 Jun 2023 16:43:00 -0700 Subject: [PATCH 1/4] Removing copying on content headers. --- tools/Custom/HttpMessageLogFormatter.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tools/Custom/HttpMessageLogFormatter.cs b/tools/Custom/HttpMessageLogFormatter.cs index 6b1b5a4cdb3..61025ba929d 100644 --- a/tools/Custom/HttpMessageLogFormatter.cs +++ b/tools/Custom/HttpMessageLogFormatter.cs @@ -41,11 +41,6 @@ await originalRequest.Content.ReadAsStreamAsync().ContinueWith(t => newRequest.Content = new StreamContent(t.Result); }).ConfigureAwait(false); - - // Copy content headers. - if (originalRequest.Content.Headers != null) - foreach (var contentHeader in originalRequest.Content.Headers) - newRequest.Content.Headers.TryAddWithoutValidation(contentHeader.Key, contentHeader.Value); } return newRequest; } From d6b11f225e070f469af85f4881f947c398cbc894 Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Tue, 13 Jun 2023 14:45:29 -0700 Subject: [PATCH 2/4] Copy content headers once. --- tools/Custom/HttpMessageLogFormatter.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tools/Custom/HttpMessageLogFormatter.cs b/tools/Custom/HttpMessageLogFormatter.cs index 61025ba929d..727ba1e5576 100644 --- a/tools/Custom/HttpMessageLogFormatter.cs +++ b/tools/Custom/HttpMessageLogFormatter.cs @@ -34,13 +34,11 @@ internal static async Task CloneAsync(this HttpRequestMessag if (originalRequest.Content != null) { // HttpClient doesn't rewind streams and we have to explicitly do so. - await originalRequest.Content.ReadAsStreamAsync().ContinueWith(t => - { - if (t.Result.CanSeek) - t.Result.Seek(0, SeekOrigin.Begin); - - newRequest.Content = new StreamContent(t.Result); - }).ConfigureAwait(false); + var ms = new MemoryStream(); + await originalRequest.Content.CopyToAsync(ms); + ms.Position = 0; + newRequest.Content = new StreamContent(ms); + originalRequest.Content.Headers?.ToList().ForEach(header => newRequest.Content.Headers.TryAddWithoutValidation(header.Key, header.Value)); } return newRequest; } From a6522337572f2606bf269f69a788e75a61488f33 Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Mon, 19 Jun 2023 13:12:17 -0700 Subject: [PATCH 3/4] Retry content header enumeration once on InvalidOperationException. --- tools/Custom/HttpMessageLogFormatter.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tools/Custom/HttpMessageLogFormatter.cs b/tools/Custom/HttpMessageLogFormatter.cs index 727ba1e5576..93322c5a5b3 100644 --- a/tools/Custom/HttpMessageLogFormatter.cs +++ b/tools/Custom/HttpMessageLogFormatter.cs @@ -38,7 +38,22 @@ internal static async Task CloneAsync(this HttpRequestMessag await originalRequest.Content.CopyToAsync(ms); ms.Position = 0; newRequest.Content = new StreamContent(ms); - originalRequest.Content.Headers?.ToList().ForEach(header => newRequest.Content.Headers.TryAddWithoutValidation(header.Key, header.Value)); + // Attempt to copy request content headers with a single retry. + // In .NET Framework, HttpHeaders dictionary is not thread safe. See https://github.com/dotnet/runtime/issues/61798. + int retryCount = 0; + int maxRetryCount = 2; + while (retryCount < maxRetryCount) + { + try + { + originalRequest.Content.Headers?.ToList().ForEach(header => newRequest.Content.Headers.TryAddWithoutValidation(header.Key, header.Value)); + retryCount = maxRetryCount; + } + catch (InvalidOperationException) + { + retryCount++; + } + } } return newRequest; } From bd11c7be04db274bbaf59f46dda0fe7288c97301 Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Mon, 19 Jun 2023 17:28:30 -0700 Subject: [PATCH 4/4] Add Pester test. --- .../v1.0/test/New-MgGroup.Recording.json | 35 +++++++++++++++++ src/Groups/v1.0/test/New-MgGroup.Tests.ps1 | 38 +++++++++++++++++++ tools/Custom/HttpMessageLogFormatter.cs | 2 +- tools/Tests/loadEnv.ps1 | 26 ++++--------- 4 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 src/Groups/v1.0/test/New-MgGroup.Recording.json create mode 100644 src/Groups/v1.0/test/New-MgGroup.Tests.ps1 diff --git a/src/Groups/v1.0/test/New-MgGroup.Recording.json b/src/Groups/v1.0/test/New-MgGroup.Recording.json new file mode 100644 index 00000000000..d61bfbeaf1d --- /dev/null +++ b/src/Groups/v1.0/test/New-MgGroup.Recording.json @@ -0,0 +1,35 @@ +{ + "New-MgGroup+[NoContext]+ShouldCreateNewGroup+$POST+https://graph.microsoft.com/v1.0/groups+1": { + "Request": { + "Method": "POST", + "RequestUri": "https://graph.microsoft.com/v1.0/groups", + "Content": "{\r\n \"displayName\": \"new-mggroup-test\",\r\n \"mailEnabled\": false,\r\n \"mailNickname\": \"unused\",\r\n \"securityEnabled\": true\r\n}", + "isContentBase64": false, + "Headers": { + }, + "ContentHeaders": { + "Content-Type": [ "application/json" ], + "Content-Length": [ "123" ] + } + }, + "Response": { + "StatusCode": 200, + "Headers": { + "Transfer-Encoding": [ "chunked" ], + "Vary": [ "Accept-Encoding" ], + "Strict-Transport-Security": [ "max-age=31536000" ], + "request-id": [ "86476695-f973-44df-8964-2a53227404ab" ], + "client-request-id": [ "aeceaf2b-98af-4fc0-a276-577a2e182727" ], + "x-ms-ags-diagnostic": [ "{\"ServerInfo\":{\"DataCenter\":\"West US 2\",\"Slice\":\"E\",\"Ring\":\"1\",\"ScaleUnit\":\"001\",\"RoleInstance\":\"MW2PEPF0000749D\"}}" ], + "WWW-Authenticate": [ "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", client_id=\"00000003-0000-0000-c000-000000000000\"" ], + "Date": [ "Mon, 19 Jun 2023 23:03:34 GMT" ] + }, + "ContentHeaders": { + "Content-Type": [ "application/json" ], + "Content-Encoding": [ "gzip" ] + }, + "Content": "{\r\n \"@odata.context\": \"https://graph.microsoft.com/v1.0/$metadata#groups/$entity\",\r\n \"id\": \"02bd9fd6-8f93-4758-87c3-1fb73740a315\",\r\n \"displayName\": \"new-mggroup-test\",\r\n \"groupTypes\": [\r\n \"Unified\"\r\n ],\r\n \"mailEnabled\": false,\r\n \"mailNickname\": \"unused\",\r\n \"securityEnabled\": true\r\n}", + "isContentBase64": false + } + } +} \ No newline at end of file diff --git a/src/Groups/v1.0/test/New-MgGroup.Tests.ps1 b/src/Groups/v1.0/test/New-MgGroup.Tests.ps1 new file mode 100644 index 00000000000..607f06c9cb5 --- /dev/null +++ b/src/Groups/v1.0/test/New-MgGroup.Tests.ps1 @@ -0,0 +1,38 @@ +BeforeAll { + if (($null -eq $TestName) -or ($TestName -contains 'New-MgGroup')) { + # Set test mode to playback. + $TestMode = 'playback' + $loadEnvPath = Join-Path $PSScriptRoot 'loadEnv.ps1' + if (-Not (Test-Path -Path $loadEnvPath)) { + $loadEnvPath = Join-Path $PSScriptRoot '..\loadEnv.ps1' + } + . ($loadEnvPath) + $TestRecordingFile = Join-Path $PSScriptRoot 'New-MgGroup.Recording.json' + $currentPath = $PSScriptRoot + while (-not $mockingPath) { + $mockingPath = Get-ChildItem -Path $currentPath -Recurse -Include 'HttpPipelineMocking.ps1' -File + $currentPath = Split-Path -Path $currentPath -Parent + } + . ($mockingPath | Select-Object -First 1).FullName + } +} + +Describe 'New-MgGroup' { + BeforeAll { + $Mock.PushDescription('New-MgGroup') + } + + Context 'Create' { + It 'ShouldCreateNewGroup' { + $CreateGroups = @() + 1..100 | ForEach-Object { + $Mock.PushScenario('ShouldCreateNewGroup') + $CreateGroups += New-MgGroup -DisplayName "new-mggroup-test" -MailEnabled:$false -MailNickname 'unused' -SecurityEnabled + } + + $CreateGroups | Should -HaveCount 100 + $CreateGroups[0].DisplayName | Should -Be "new-mggroup-test" + $CreateGroups[0].MailEnabled | Should -BeFalse + } + } +} diff --git a/tools/Custom/HttpMessageLogFormatter.cs b/tools/Custom/HttpMessageLogFormatter.cs index 93322c5a5b3..b44c3c7da73 100644 --- a/tools/Custom/HttpMessageLogFormatter.cs +++ b/tools/Custom/HttpMessageLogFormatter.cs @@ -39,7 +39,7 @@ internal static async Task CloneAsync(this HttpRequestMessag ms.Position = 0; newRequest.Content = new StreamContent(ms); // Attempt to copy request content headers with a single retry. - // In .NET Framework, HttpHeaders dictionary is not thread safe. See https://github.com/dotnet/runtime/issues/61798. + // HttpHeaders dictionary is not thread-safe when targeting anything below .NET 7. For more information, see https://github.com/dotnet/runtime/issues/61798. int retryCount = 0; int maxRetryCount = 2; while (retryCount < maxRetryCount) diff --git a/tools/Tests/loadEnv.ps1 b/tools/Tests/loadEnv.ps1 index c5ed68dee1e..e9d8834c948 100644 --- a/tools/Tests/loadEnv.ps1 +++ b/tools/Tests/loadEnv.ps1 @@ -11,24 +11,12 @@ # See the License for the specific language governing permissions and # limitations under the License. # ---------------------------------------------------------------------------------- -$envFile = 'env.json' -if ($TestMode -eq 'live') { - $envFile = 'localEnv.json' -} -if (Test-Path -Path (Join-Path $PSScriptRoot $envFile)) { - $envFilePath = Join-Path $PSScriptRoot $envFile -} else { - $envFilePath = Join-Path $PSScriptRoot '..\$envFile' +if ($TestMode -eq 'live' -or $TestMode -eq 'record') { + Connect-MgGraph -ClientId $env:testApp_clientId -TenantId $env:testApp_tenantId -CertificateThumbprint $env:testApp_certThumbprint +} +else { + # Use dummy access token to run Pester tests. + # Provide the dummy access token to $env:testApp_dummyAccessToken in your environment variable. + Connect-MgGraph -AccessToken (ConvertTo-SecureString -String $env:testApp_dummyAccessToken -AsPlainText) } -$env = @{} -if (Test-Path -Path $envFilePath) { - # Load dummy auth configuration. This is used to run Pester tests. - $env = Get-Content (Join-Path $PSScriptRoot $envFile) | ConvertFrom-Json -AsHashTable - [Microsoft.Graph.PowerShell.Authentication.GraphSession]::Instance.AuthContext = New-Object Microsoft.Graph.PowerShell.Authentication.AuthContext -Property @{ - ClientId = $env.ClientId - TenantId = $env.TenantId - AuthType = [Microsoft.Graph.PowerShell.Authentication.AuthenticationType]::UserProvidedAccessToken - TokenCredentialType = [Microsoft.Graph.PowerShell.Authentication.TokenCredentialType]::UserProvidedAccessToken - } -} \ No newline at end of file