tests/Grant-MsecAzureDevOpsPermission.Tests.ps1
|
#Requires -Module Pester # # Tests for Grant-MsecAzureDevOpsPermission. # # This is the one command that WRITES into Azure DevOps, and the traps it guards are the ones # that make a wrong grant look like a right one: # # THE TWO PERMISSION SYSTEMS ARE NOT INTERCHANGEABLE. An allow on the ServiceEndpoints # namespace is accepted, stored, returned by the ACL API - and confers nothing. Passing both # -Permission and -RoleName must be refused rather than guessed at. # # NOTHING IS HARDCODED. The bit is resolved by NAME from namespace metadata, so a renumbered # bit fails loudly instead of silently granting a different permission. # # IT WRITES ONLY UNDER ShouldProcess, and merge=true so other identities' entries survive. BeforeAll { Import-Module (Join-Path $PSScriptRoot '..' 'msec.psm1') -Force -ErrorAction Stop } AfterAll { Remove-Module msec -Force -ErrorAction SilentlyContinue } Describe 'Grant-MsecAzureDevOpsPermission' { BeforeEach { InModuleScope msec { Mock Get-AzContext -MockWith { [pscustomobject]@{ Account = 'me@contoso.com' } } Mock Get-AzAccessToken -MockWith { [pscustomobject]@{ Token = 'user-token' } } } } It 'runs as the signed-in user, not as the msec app' { # The app is usually the GRANTEE. An identity that could grant itself permissions makes # the whole exercise circular, so this deliberately never touches Get-MsecAccessToken. $body = (Get-Command Grant-MsecAzureDevOpsPermission).Definition $body | Should -Match 'Get-AzAccessToken' $body | Should -Not -Match 'Get-MsecAccessToken' $body | Should -Not -Match 'Invoke-MsecGraphRequest' } It 'needs no personal access token' { # An earlier version took one. The namespace, ACL and identity APIs all accept an # ordinary Entra token, verified against all three before the PAT was removed. (Get-Command Grant-MsecAzureDevOpsPermission).Parameters.Keys | Should -Not -Contain 'Pat' (Get-Command Grant-MsecAzureDevOpsPermission).Definition | Should -Not -Match "Authorization = 'Basic" } It 'refuses to guess between the two permission systems' { InModuleScope msec { Mock Invoke-RestMethod -MockWith { @{ value = @(@{ name = 'Git Repositories'; namespaceId = 'ns-1'; structureValue = 1 actions = @(@{ name = 'GenericRead'; bit = 2; displayName = 'Read' }) }) } } { Grant-MsecAzureDevOpsPermission -Organization o -Identity 'G' -Permission GenericRead -RoleName Reader -Confirm:$false } | Should -Throw '*not both*' } } It 'resolves the bit by NAME and refuses an unknown permission' { InModuleScope msec { Mock Invoke-RestMethod -MockWith { @{ value = @(@{ name = 'Git Repositories'; namespaceId = 'ns-1'; structureValue = 1 actions = @(@{ name = 'GenericRead'; bit = 2; displayName = 'Read' }) }) } } # A renumbered or renamed bit must fail loudly, never silently grant a different one. { Grant-MsecAzureDevOpsPermission -Organization o -Identity 'G' -Permission NotAThing -Confirm:$false } | Should -Throw "*not a permission*" } } It 'refuses a namespace whose root token has not been proven' { InModuleScope msec { Mock Invoke-RestMethod -MockWith { @{ value = @( @{ name = 'Unproven'; namespaceId = 'ns-9'; structureValue = 1 actions = @(@{ name = 'GenericRead'; bit = 2; displayName = 'Read' }) } ) } } # 'repoV2' works and 'repoV2/' returns 400 for the same body - one character decides # organization-wide versus per-project. Unknown grammars are refused, not guessed. { Grant-MsecAzureDevOpsPermission -Organization o -Identity 'G' -Namespace Unproven ` -Permission GenericRead -Confirm:$false } | Should -Throw '*token grammar*' } } It 'emits permission names as objects for -ListPermissions, and writes nothing' { $rows = InModuleScope msec { Mock Invoke-RestMethod -MockWith { @{ value = @(@{ name = 'Git Repositories'; namespaceId = 'ns-1'; structureValue = 1 actions = @( @{ name = 'GenericRead'; bit = 2; displayName = 'Read' } @{ name = 'ViewAdvSecAlerts'; bit = 65536; displayName = 'View alerts' }) }) } } @(Grant-MsecAzureDevOpsPermission -Organization o -ListPermissions) } $rows.Count | Should -Be 2 ($rows | Where-Object Name -eq 'ViewAdvSecAlerts').Bit | Should -Be 65536 } It 'declares High confirm impact so a bare call prompts' { $meta = [System.Management.Automation.CommandMetadata](Get-Command Grant-MsecAzureDevOpsPermission) $meta.SupportsShouldProcess | Should -BeTrue $meta.ConfirmImpact | Should -Be 'High' } It 'has no -Apply switch - WhatIf and Confirm replaced it' { (Get-Command Grant-MsecAzureDevOpsPermission).Parameters.Keys | Should -Not -Contain 'Apply' } } |