Skip to content

Commit ec509a9

Browse files
committed
feat(tests): add tests for Connect-HaloAPI to validate Key Vault integration and error handling
1 parent b3a3902 commit ec509a9

1 file changed

Lines changed: 237 additions & 0 deletions

File tree

Tests/HaloAPI.Unit.Tests.ps1

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,243 @@ Describe 'Connect-HaloAPI' {
10011001
}
10021002
}
10031003

1004+
It 'loads secrets from Key Vault using interactive Azure login when no identity is provided' {
1005+
Mock -CommandName 'Connect-AzAccount' -ModuleName 'HaloAPI' -MockWith {} -ParameterFilter {
1006+
-not $Identity
1007+
}
1008+
1009+
Mock -CommandName 'Get-AzKeyVaultSecret' -ModuleName 'HaloAPI' -MockWith {
1010+
switch ($Name) {
1011+
'halo_URL' { [pscustomobject]@{ SecretValueText = 'https://interactive.example/' } }
1012+
'halo_ClientID' { [pscustomobject]@{ SecretValueText = 'interactive-client' } }
1013+
'halo_ClientSecret' { [pscustomobject]@{ SecretValueText = 'interactive-secret' } }
1014+
}
1015+
}
1016+
1017+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1018+
[pscustomobject]@{ content = '{"auth_url":"https://auth.example/oauth2","tenant_id":"tenantFromAuthInfo"}' }
1019+
} -ParameterFilter {
1020+
$Method -eq 'GET'
1021+
}
1022+
1023+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1024+
[pscustomobject]@{ Content = '{"token_type":"Bearer","access_token":"abc","expires_in":3600,"refresh_token":"ref","id_token":"id"}' }
1025+
} -ParameterFilter {
1026+
$Method -eq 'POST'
1027+
}
1028+
1029+
$Result = Connect-HaloAPI -URL 'https://placeholder.example/' -ClientID 'unused' -ClientSecret 'unused' -Scopes 'all' -UseKeyVault $true -VaultName 'vault' -SecretName 'halo'
1030+
1031+
$Result | Should -BeTrue
1032+
Should -Invoke -CommandName 'Connect-AzAccount' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
1033+
-not $Identity
1034+
}
1035+
}
1036+
1037+
It 'saves secrets to Key Vault using interactive Azure login when no identity is provided' {
1038+
Mock -CommandName 'Connect-AzAccount' -ModuleName 'HaloAPI' -MockWith {} -ParameterFilter {
1039+
-not $Identity
1040+
}
1041+
1042+
Mock -CommandName 'Set-AzKeyVaultSecret' -ModuleName 'HaloAPI' -MockWith {
1043+
[pscustomobject]@{ Name = $Name }
1044+
}
1045+
1046+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1047+
[pscustomobject]@{ content = '{"auth_url":"https://auth.example/oauth2","tenant_id":"tenantFromAuthInfo"}' }
1048+
} -ParameterFilter {
1049+
$Method -eq 'GET'
1050+
}
1051+
1052+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1053+
[pscustomobject]@{ Content = '{"token_type":"Bearer","access_token":"abc","expires_in":3600,"refresh_token":"ref","id_token":"id"}' }
1054+
} -ParameterFilter {
1055+
$Method -eq 'POST'
1056+
}
1057+
1058+
$Result = Connect-HaloAPI -URL 'https://example.halo/' -ClientID 'client-save' -ClientSecret 'secret-save' -Scopes 'all' -SaveToKeyVault $true -VaultName 'vault' -SecretName 'halo'
1059+
1060+
$Result | Should -BeTrue
1061+
Should -Invoke -CommandName 'Connect-AzAccount' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
1062+
-not $Identity
1063+
}
1064+
}
1065+
1066+
It 'falls back to auth/token without tenant when authinfo response is empty and no tenant is provided' {
1067+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1068+
[pscustomobject]@{ content = $null }
1069+
} -ParameterFilter {
1070+
$Method -eq 'GET'
1071+
}
1072+
1073+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1074+
[pscustomobject]@{ Content = '{"token_type":"Bearer","access_token":"abc","expires_in":3600,"refresh_token":"ref","id_token":"id"}' }
1075+
} -ParameterFilter {
1076+
$Method -eq 'POST'
1077+
}
1078+
1079+
$Result = Connect-HaloAPI -URL 'https://example.halo/' -ClientID 'client-fallback' -ClientSecret 'secret-fallback' -Scopes 'all'
1080+
1081+
$Result | Should -BeTrue
1082+
Should -Invoke -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
1083+
$Method -eq 'POST' -and $Uri -eq 'https://example.halo/auth/token'
1084+
}
1085+
}
1086+
1087+
It 'rethrows non-throttling HttpResponseException from auth info lookup' {
1088+
$Response = [System.Net.Http.HttpResponseMessage]::new([System.Net.HttpStatusCode]::BadRequest)
1089+
$HttpException = [Microsoft.PowerShell.Commands.HttpResponseException]::new('bad request', $Response)
1090+
1091+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1092+
throw $HttpException
1093+
} -ParameterFilter {
1094+
$Method -eq 'GET'
1095+
}
1096+
1097+
{
1098+
Connect-HaloAPI -URL 'https://example.halo/' -ClientID 'client-authinfo' -ClientSecret 'secret-authinfo' -Scopes 'all'
1099+
} | Should -Throw
1100+
1101+
Should -Invoke -CommandName 'New-HaloError' -ModuleName 'HaloAPI' -Times 0 -Exactly -ParameterFilter {
1102+
$HasResponse
1103+
}
1104+
}
1105+
1106+
It 'routes unexpected auth info lookup errors through New-HaloError and then falls back to the default auth path' {
1107+
$script:AuthInfoErrorCallCount = 0
1108+
1109+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1110+
$script:AuthInfoErrorCallCount += 1
1111+
if ($script:AuthInfoErrorCallCount -eq 1) {
1112+
throw [System.Exception]::new('auth info failed')
1113+
}
1114+
1115+
[pscustomobject]@{ content = $null }
1116+
} -ParameterFilter {
1117+
$Method -eq 'GET'
1118+
}
1119+
1120+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1121+
[pscustomobject]@{ Content = '{"token_type":"Bearer","access_token":"abc","expires_in":3600,"refresh_token":"ref","id_token":"id"}' }
1122+
} -ParameterFilter {
1123+
$Method -eq 'POST'
1124+
}
1125+
1126+
$Result = Connect-HaloAPI -URL 'https://example.halo/' -ClientID 'client-authinfo' -ClientSecret 'secret-authinfo' -Scopes 'all'
1127+
1128+
$Result | Should -BeTrue
1129+
Should -Invoke -CommandName 'New-HaloError' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
1130+
$HasResponse
1131+
}
1132+
}
1133+
1134+
It 'reports auth info retry exhaustion after repeated throttling before falling back to the default auth path' {
1135+
$Response = [System.Net.Http.HttpResponseMessage]::new([System.Net.HttpStatusCode]::TooManyRequests)
1136+
$HttpException = [Microsoft.PowerShell.Commands.HttpResponseException]::new('throttled', $Response)
1137+
1138+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1139+
throw $HttpException
1140+
} -ParameterFilter {
1141+
$Method -eq 'GET'
1142+
}
1143+
1144+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1145+
[pscustomobject]@{ Content = '{"token_type":"Bearer","access_token":"abc","expires_in":3600,"refresh_token":"ref","id_token":"id"}' }
1146+
} -ParameterFilter {
1147+
$Method -eq 'POST'
1148+
}
1149+
1150+
$Result = Connect-HaloAPI -URL 'https://example.halo/' -ClientID 'client-authinfo' -ClientSecret 'secret-authinfo' -Scopes 'all'
1151+
1152+
$Result | Should -BeTrue
1153+
Should -Invoke -CommandName 'Start-Sleep' -ModuleName 'HaloAPI' -Times 10 -Exactly -ParameterFilter {
1154+
$Seconds -eq 5
1155+
}
1156+
Should -Invoke -CommandName 'New-HaloError' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
1157+
$ModuleMessage -eq 'Retried auth info request 10 times, request unsuccessful.'
1158+
}
1159+
}
1160+
1161+
It 'rethrows non-throttling HttpResponseException from token request' {
1162+
$Response = [System.Net.Http.HttpResponseMessage]::new([System.Net.HttpStatusCode]::BadRequest)
1163+
$HttpException = [Microsoft.PowerShell.Commands.HttpResponseException]::new('bad request', $Response)
1164+
1165+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1166+
[pscustomobject]@{ content = '{"auth_url":"https://auth.example/oauth2","tenant_id":"tenantFromAuthInfo"}' }
1167+
} -ParameterFilter {
1168+
$Method -eq 'GET'
1169+
}
1170+
1171+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1172+
throw $HttpException
1173+
} -ParameterFilter {
1174+
$Method -eq 'POST'
1175+
}
1176+
1177+
{
1178+
Connect-HaloAPI -URL 'https://example.halo/' -ClientID 'client-token' -ClientSecret 'secret-token' -Scopes 'all'
1179+
} | Should -Throw
1180+
1181+
Should -Invoke -CommandName 'New-HaloError' -ModuleName 'HaloAPI' -Times 0 -Exactly -ParameterFilter {
1182+
-not $HasResponse -and $ErrorRecord
1183+
}
1184+
}
1185+
1186+
It 'routes unexpected token request errors through New-HaloError before succeeding on retry' {
1187+
$script:TokenErrorCallCount = 0
1188+
1189+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1190+
[pscustomobject]@{ content = '{"auth_url":"https://auth.example/oauth2","tenant_id":"tenantFromAuthInfo"}' }
1191+
} -ParameterFilter {
1192+
$Method -eq 'GET'
1193+
}
1194+
1195+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1196+
$script:TokenErrorCallCount += 1
1197+
if ($script:TokenErrorCallCount -eq 1) {
1198+
throw [System.Exception]::new('token failed')
1199+
}
1200+
1201+
[pscustomobject]@{ Content = '{"token_type":"Bearer","access_token":"abc","expires_in":3600,"refresh_token":"ref","id_token":"id"}' }
1202+
} -ParameterFilter {
1203+
$Method -eq 'POST'
1204+
}
1205+
1206+
$Result = Connect-HaloAPI -URL 'https://example.halo/' -ClientID 'client-token' -ClientSecret 'secret-token' -Scopes 'all'
1207+
1208+
$Result | Should -BeTrue
1209+
Should -Invoke -CommandName 'New-HaloError' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
1210+
-not $HasResponse -and $ErrorRecord
1211+
}
1212+
}
1213+
1214+
It 'reports auth retry exhaustion after repeated throttling and returns false' {
1215+
$Response = [System.Net.Http.HttpResponseMessage]::new([System.Net.HttpStatusCode]::TooManyRequests)
1216+
$HttpException = [Microsoft.PowerShell.Commands.HttpResponseException]::new('throttled', $Response)
1217+
1218+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1219+
[pscustomobject]@{ content = '{"auth_url":"https://auth.example/oauth2","tenant_id":"tenantFromAuthInfo"}' }
1220+
} -ParameterFilter {
1221+
$Method -eq 'GET'
1222+
}
1223+
1224+
Mock -CommandName 'Invoke-WebRequest' -ModuleName 'HaloAPI' -MockWith {
1225+
throw $HttpException
1226+
} -ParameterFilter {
1227+
$Method -eq 'POST'
1228+
}
1229+
1230+
$Result = Connect-HaloAPI -URL 'https://example.halo/' -ClientID 'client-token' -ClientSecret 'secret-token' -Scopes 'all'
1231+
1232+
$Result | Should -BeFalse
1233+
Should -Invoke -CommandName 'Start-Sleep' -ModuleName 'HaloAPI' -Times 10 -Exactly -ParameterFilter {
1234+
$Seconds -eq 5
1235+
}
1236+
Should -Invoke -CommandName 'New-HaloError' -ModuleName 'HaloAPI' -Times 1 -Exactly -ParameterFilter {
1237+
$ModuleMessage -eq 'Retried auth request 10 times, request unsuccessful.'
1238+
}
1239+
}
1240+
10041241
It 'retries the auth info request after a throttled response and does not report a failure after success' {
10051242
$script:AuthInfoCallCount = 0
10061243
$Response = [System.Net.Http.HttpResponseMessage]::new([System.Net.HttpStatusCode]::TooManyRequests)

0 commit comments

Comments
 (0)