Skip to content

Commit b3a3902

Browse files
committed
feat(tests): enhance Connect-HaloAPI tests for Key Vault integration and retry logic
1 parent db894fa commit b3a3902

3 files changed

Lines changed: 185 additions & 7 deletions

File tree

Public/Connect-HaloAPI.ps1

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ function Connect-HaloAPI {
132132
Method = 'GET'
133133
Headers = $AdditionalHeaders
134134
}
135+
$AuthInfoRetries = 0
135136
do {
136137
$AuthInfoRetries++
137138
try {
@@ -149,9 +150,9 @@ function Connect-HaloAPI {
149150
} catch {
150151
New-HaloError -ErrorRecord $_ -HasResponse
151152
}
152-
} while ((-not $AuthInfoResponse) -and ($AuthRetries -lt 10))
153-
if ($AuthInfoRetries -gt 1) {
154-
New-HaloError -ModuleMessage ('Retried auth info request {0} times, request unsuccessful.' -f $Retries)
153+
} while ((-not $AuthInfoResponse) -and ($AuthInfoRetries -lt 10))
154+
if ((-not $AuthInfoResponse) -and ($AuthInfoRetries -gt 1)) {
155+
New-HaloError -ModuleMessage ('Retried auth info request {0} times, request unsuccessful.' -f $AuthInfoRetries)
155156
}
156157
if ($AuthInfoResponse.content) {
157158
$AuthInfo = $AuthInfoResponse.content | ConvertFrom-Json
@@ -215,6 +216,7 @@ function Connect-HaloAPI {
215216
ContentType = 'application/x-www-form-urlencoded'
216217
Headers = $AdditionalHeaders
217218
}
219+
$AuthRetries = 0
218220
do {
219221
$AuthRetries++
220222
try {
@@ -255,10 +257,10 @@ function Connect-HaloAPI {
255257
New-HaloError -ErrorRecord $_
256258
}
257259
} while ((-not $Authenticated) -and ($AuthRetries -lt 10))
260+
if ((-not $Authenticated) -and ($AuthRetries -gt 1)) {
261+
New-HaloError -ModuleMessage ('Retried auth request {0} times, request unsuccessful.' -f $AuthRetries)
262+
}
258263
if (!$NoConfirm) {
259264
return $Authenticated
260265
}
261-
if ($AuthRetries -gt 1) {
262-
New-HaloError -ModuleMessage ('Retried auth request {0} times, request unsuccessful.' -f $Retries)
263-
}
264266
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ This is the code for a [PowerShell](https://microsoft.com/powershell) module for
2424
* [HaloITSM](https://haloitsm.com/)
2525
* [HaloServiceDesk](https://haloservicedesk.com/)
2626

27-
The module is written for [PowerShell 7](https://docs.microsoft.com/en-us/powershell/scripting/whats-new/what-s-new-in-powershell-71?view=powershell-7.1). **It is not compatible with Windows PowerShell 5.1 and never will be.**. This module is licensed under the [MIT](https://haloapi.mit-license.org/) license.
27+
The module is written for [PowerShell 7](https://docs.microsoft.com/en-us/powershell/scripting/whats-new/what-s-new-in-powershell-71?view=powershell-7.1). **It is not compatible with Windows PowerShell 5.1 and there are no current plans to change this**. This module is licensed under the [MIT](https://mit.license.homotechsual.dev/) license.
2828

2929
## What does it do?
3030

Tests/HaloAPI.Unit.Tests.ps1

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,7 @@ Describe 'Connect-HaloAPI' {
764764
Mock -CommandName 'New-HaloGETRequest' -ModuleName 'HaloAPI' -MockWith {
765765
@([pscustomobject]@{ id = 1; name = 'Lookup' })
766766
}
767+
Mock -CommandName 'Start-Sleep' -ModuleName 'HaloAPI' -MockWith {}
767768
}
768769

769770
It 'uses authinfo endpoint, applies provided tenant, and joins scopes for token request' {
@@ -919,6 +920,181 @@ Describe 'Connect-HaloAPI' {
919920
$Method -eq 'POST' -and $Headers['X-Test'] -eq 'true'
920921
}
921922
}
923+
924+
It 'loads URL, client ID, and client secret from Key Vault using managed identity' {
925+
Mock -CommandName 'Connect-AzAccount' -ModuleName 'HaloAPI' -MockWith {} -ParameterFilter {
926+
$Identity
927+
}
928+
929+
Mock -CommandName 'Get-AzKeyVaultSecret' -ModuleName 'HaloAPI' -MockWith {
930+
switch ($Name) {
931+
'halo_URL' { [pscustomobject]@{ SecretValueText = 'https://vault.example/' } }
932+
'halo_ClientID' { [pscustomobject]@{ SecretValueText = 'vault-client' } }
933+
'halo_ClientSecret' { [pscustomobject]@{ SecretValueText = 'vault-secret' } }
934+
}
935+
}
936+
937+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
938+
[pscustomobject]@{ content = '{"auth_url":"https://auth.example/oauth2","tenant_id":"vaultTenant"}' }
939+
} -ParameterFilter {
940+
$Method -eq 'GET'
941+
}
942+
943+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
944+
[pscustomobject]@{ Content = '{"token_type":"Bearer","access_token":"abc","expires_in":3600,"refresh_token":"ref","id_token":"id"}' }
945+
} -ParameterFilter {
946+
$Method -eq 'POST'
947+
}
948+
949+
$Result = Connect-HaloAPI -URL 'https://placeholder.example/' -ClientID 'unused' -ClientSecret 'unused' -Scopes 'all' -UseKeyVault $true -VaultName 'vault' -SecretName 'halo' -Identity 'mi-1'
950+
951+
$Result | Should -BeTrue
952+
Should -Invoke -CommandName 'Connect-AzAccount' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
953+
$Identity
954+
}
955+
Should -Invoke -CommandName 'Get-AzKeyVaultSecret' -ModuleName 'HaloAPI' -Times 3 -Exactly -ParameterFilter {
956+
$VaultName -eq 'vault'
957+
}
958+
Should -Invoke -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
959+
$Method -eq 'GET' -and $Uri -eq 'https://vault.example/api/authinfo'
960+
}
961+
Should -Invoke -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
962+
$Method -eq 'POST' -and $Body.client_id -eq 'vault-client' -and $Body.client_secret -eq 'vault-secret'
963+
}
964+
}
965+
966+
It 'saves URL, client ID, and client secret to Key Vault before authenticating' {
967+
Mock -CommandName 'Connect-AzAccount' -ModuleName 'HaloAPI' -MockWith {} -ParameterFilter {
968+
$Identity
969+
}
970+
971+
Mock -CommandName 'Set-AzKeyVaultSecret' -ModuleName 'HaloAPI' -MockWith {
972+
[pscustomobject]@{ Name = $Name }
973+
}
974+
975+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
976+
[pscustomobject]@{ content = '{"auth_url":"https://auth.example/oauth2","tenant_id":"tenantFromAuthInfo"}' }
977+
} -ParameterFilter {
978+
$Method -eq 'GET'
979+
}
980+
981+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
982+
[pscustomobject]@{ Content = '{"token_type":"Bearer","access_token":"abc","expires_in":3600,"refresh_token":"ref","id_token":"id"}' }
983+
} -ParameterFilter {
984+
$Method -eq 'POST'
985+
}
986+
987+
$Result = Connect-HaloAPI -URL 'https://example.halo/' -ClientID 'client-save' -ClientSecret 'secret-save' -Scopes 'all' -SaveToKeyVault $true -VaultName 'vault' -SecretName 'halo' -Identity 'mi-2'
988+
989+
$Result | Should -BeTrue
990+
Should -Invoke -CommandName 'Connect-AzAccount' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
991+
$Identity
992+
}
993+
Should -Invoke -CommandName 'Set-AzKeyVaultSecret' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
994+
$VaultName -eq 'vault' -and $Name -eq 'halo_URL'
995+
}
996+
Should -Invoke -CommandName 'Set-AzKeyVaultSecret' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
997+
$VaultName -eq 'vault' -and $Name -eq 'halo_ClientID'
998+
}
999+
Should -Invoke -CommandName 'Set-AzKeyVaultSecret' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
1000+
$VaultName -eq 'vault' -and $Name -eq 'halo_ClientSecret'
1001+
}
1002+
}
1003+
1004+
It 'retries the auth info request after a throttled response and does not report a failure after success' {
1005+
$script:AuthInfoCallCount = 0
1006+
$Response = [System.Net.Http.HttpResponseMessage]::new([System.Net.HttpStatusCode]::TooManyRequests)
1007+
$HttpException = [Microsoft.PowerShell.Commands.HttpResponseException]::new('throttled', $Response)
1008+
1009+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1010+
$script:AuthInfoCallCount += 1
1011+
if ($script:AuthInfoCallCount -eq 1) {
1012+
throw $HttpException
1013+
}
1014+
1015+
[pscustomobject]@{ content = '{"auth_url":"https://auth.example/oauth2","tenant_id":"tenantFromAuthInfo"}' }
1016+
} -ParameterFilter {
1017+
$Method -eq 'GET'
1018+
}
1019+
1020+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1021+
[pscustomobject]@{ Content = '{"token_type":"Bearer","access_token":"abc","expires_in":3600,"refresh_token":"ref","id_token":"id"}' }
1022+
} -ParameterFilter {
1023+
$Method -eq 'POST'
1024+
}
1025+
1026+
$Result = Connect-HaloAPI -URL 'https://example.halo/' -ClientID 'client-retry' -ClientSecret 'secret-retry' -Scopes 'all'
1027+
1028+
$Result | Should -BeTrue
1029+
Should -Invoke -CommandName 'Start-Sleep' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
1030+
$Seconds -eq 5
1031+
}
1032+
Should -Invoke -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -Times 2 -Exactly -ParameterFilter {
1033+
$Method -eq 'GET'
1034+
}
1035+
Should -Invoke -CommandName 'New-HaloError' -ModuleName 'HaloAPI' -Times 0 -Exactly -ParameterFilter {
1036+
$ModuleMessage -like 'Retried auth info request*'
1037+
}
1038+
}
1039+
1040+
It 'retries the token request after a throttled response and does not report a failure after success' {
1041+
$script:TokenCallCount = 0
1042+
$Response = [System.Net.Http.HttpResponseMessage]::new([System.Net.HttpStatusCode]::TooManyRequests)
1043+
$HttpException = [Microsoft.PowerShell.Commands.HttpResponseException]::new('throttled', $Response)
1044+
1045+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1046+
[pscustomobject]@{ content = '{"auth_url":"https://auth.example/oauth2","tenant_id":"tenantFromAuthInfo"}' }
1047+
} -ParameterFilter {
1048+
$Method -eq 'GET'
1049+
}
1050+
1051+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1052+
$script:TokenCallCount += 1
1053+
if ($script:TokenCallCount -eq 1) {
1054+
throw $HttpException
1055+
}
1056+
1057+
[pscustomobject]@{ Content = '{"token_type":"Bearer","access_token":"abc","expires_in":3600,"refresh_token":"ref","id_token":"id"}' }
1058+
} -ParameterFilter {
1059+
$Method -eq 'POST'
1060+
}
1061+
1062+
$Result = Connect-HaloAPI -URL 'https://example.halo/' -ClientID 'client-retry' -ClientSecret 'secret-retry' -Scopes 'all'
1063+
1064+
$Result | Should -BeTrue
1065+
Should -Invoke -CommandName 'Start-Sleep' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
1066+
$Seconds -eq 5
1067+
}
1068+
Should -Invoke -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -Times 2 -Exactly -ParameterFilter {
1069+
$Method -eq 'POST'
1070+
}
1071+
Should -Invoke -CommandName 'New-HaloError' -ModuleName 'HaloAPI' -Times 0 -Exactly -ParameterFilter {
1072+
$ModuleMessage -like 'Retried auth request*'
1073+
}
1074+
}
1075+
1076+
It 'reports a lookup initialisation failure when lookup types cannot be retrieved' {
1077+
Mock -CommandName 'Get-HaloLookup' -ModuleName 'HaloAPI' -MockWith { $null }
1078+
1079+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1080+
[pscustomobject]@{ content = '{"auth_url":"https://auth.example/oauth2","tenant_id":"tenantFromAuthInfo"}' }
1081+
} -ParameterFilter {
1082+
$Method -eq 'GET'
1083+
}
1084+
1085+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1086+
[pscustomobject]@{ Content = '{"token_type":"Bearer","access_token":"abc","expires_in":3600,"refresh_token":"ref","id_token":"id"}' }
1087+
} -ParameterFilter {
1088+
$Method -eq 'POST'
1089+
}
1090+
1091+
$Result = Connect-HaloAPI -URL 'https://example.halo/' -ClientID 'client-lookup' -ClientSecret 'secret-lookup' -Scopes 'all'
1092+
1093+
$Result | Should -BeTrue
1094+
Should -Invoke -CommandName 'New-HaloError' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
1095+
$ModuleMessage -eq 'Could not retrieve lookup types from Halo.'
1096+
}
1097+
}
9221098
}
9231099

9241100
Describe 'Get-TokenExpiry' {

0 commit comments

Comments
 (0)