@@ -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
9241100Describe ' Get-TokenExpiry' {
0 commit comments