Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/test-powershell.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Test PowerShell

on:
push:
branches:
- master
paths-ignore:
- '*.md'
- 'Docs/**'
- 'Examples/**'
- '.gitignore'
pull_request:
branches:
- master
Comment on lines +12 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Trigger workflow on main instead of master

This workflow is scoped to master, but this repository’s active development branch is main (the branch with recent merge commits), so PRs targeting main will not run these new checks. That means the intended PR gate is effectively disabled unless someone runs workflow_dispatch manually, which defeats the purpose of adding automated regression checks.

Useful? React with 👍 / 👎.

workflow_dispatch:

jobs:
test-windows-ps51:
name: Windows PowerShell 5.1
runs-on: windows-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Run tests
shell: powershell
run: ./GraphEssentials.Tests.ps1

test-windows-ps7:
name: PowerShell 7
runs-on: windows-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Run tests
shell: pwsh
run: ./GraphEssentials.Tests.ps1
26 changes: 26 additions & 0 deletions GraphEssentials.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
$PrimaryModule = Get-ChildItem -Path $PSScriptRoot -Filter '*.psd1' -File -ErrorAction Stop
if ($PrimaryModule.Count -ne 1) {
throw 'Expected exactly one PSD1 file in the repository root.'
}

$availablePester = Get-Module -ListAvailable -Name Pester | Where-Object {
$_.Version -ge [version] '5.0.0'
} | Select-Object -First 1

if (-not $availablePester) {
Install-Module -Name Pester -Scope CurrentUser -Force -AllowClobber -SkipPublisherCheck -ErrorAction Stop
}

Import-Module Pester -Force -ErrorAction Stop

$configuration = [PesterConfiguration]::Default
$configuration.Run.Path = (Join-Path $PSScriptRoot 'Tests')
$configuration.Run.Exit = $true
$configuration.Should.ErrorAction = 'Continue'
$configuration.CodeCoverage.Enabled = $false
$configuration.Output.Verbosity = 'Detailed'

$result = Invoke-Pester -Configuration $configuration
if ($result.FailedCount -gt 0) {
throw "$($result.FailedCount) tests failed."
}
55 changes: 55 additions & 0 deletions Tests/Get-GraphEssentialsErrorDetails.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
BeforeAll {
. (Join-Path $PSScriptRoot '..\Private\Get-GraphEssentialsErrorDetails.ps1')

function New-TestErrorRecord {
param(
[string] $Message
)

try {
throw [System.Exception]::new($Message)
} catch {
return $_
}
}
}

Describe 'Get-GraphEssentialsErrorDetails' {
It 'detects nested permission scope errors from Graph SDK exception text' {
$errorRecord = New-TestErrorRecord -Message @'
Status: 403 (Forbidden)
ErrorCode: UnknownError
{"errorCode":"PermissionScopeNotGranted","message":"Authorization failed due to missing permission scopes"}
'@

$result = $errorRecord | Get-GraphEssentialsErrorDetails -FunctionName 'Test-GraphEssentials'

$result.StatusCode | Should -Be 403
$result.Code | Should -Be 'PermissionScopeNotGranted'
$result.IsPermissionDenied | Should -BeTrue
$result.Message | Should -Match 'missing permission scopes'
}

It 'detects missing resources' {
$errorRecord = New-TestErrorRecord -Message @'
Status: 404 (NotFound)
ErrorCode: Request_ResourceNotFound
Message: Resource could not be found
'@

$result = $errorRecord | Get-GraphEssentialsErrorDetails -FunctionName 'Test-GraphEssentials'

$result.StatusCode | Should -Be 404
$result.Code | Should -Be 'Request_ResourceNotFound'
$result.IsNotFound | Should -BeTrue
}

It 'detects transient transport failures' {
$errorRecord = New-TestErrorRecord -Message 'Received an unexpected EOF or 0 bytes from the transport stream.'

$result = $errorRecord | Get-GraphEssentialsErrorDetails -FunctionName 'Test-GraphEssentials'

$result.IsTransient | Should -BeTrue
$result.Message | Should -Match 'unexpected EOF'
}
}
48 changes: 48 additions & 0 deletions Tests/Get-MyRoleHistory.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
BeforeAll {
. (Join-Path $PSScriptRoot '..\Private\Get-GraphEssentialsErrorDetails.ps1')
. (Join-Path $PSScriptRoot '..\Public\Get-MyRoleHistory.ps1')

foreach ($commandName in @(
'Get-MgUser'
'Get-MgGroup'
'Get-MgServicePrincipal'
'Get-MgRoleManagementDirectoryRoleDefinition'
'Get-MgRoleManagementDirectoryRoleAssignmentScheduleRequest'
'Get-MgRoleManagementDirectoryRoleEligibilityScheduleRequest'
)) {
if (-not (Get-Command -Name $commandName -ErrorAction SilentlyContinue)) {
Set-Item -Path ("Function:\" + $commandName) -Value { throw 'Test stub should be mocked.' }
}
}
}

Describe 'Get-MyRoleHistory' {
BeforeEach {
Mock -CommandName Get-MgUser -MockWith { @() }
Mock -CommandName Get-MgGroup -MockWith { @() }
Mock -CommandName Get-MgServicePrincipal -MockWith { @() }
Mock -CommandName Get-MgRoleManagementDirectoryRoleDefinition -MockWith { @() }
Mock -CommandName Get-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -MockWith {
throw [System.Exception]::new(@'
Status: 403 (Forbidden)
ErrorCode: UnknownError
{"errorCode":"PermissionScopeNotGranted","message":"Authorization failed due to missing permission scopes"}
'@)
}
Mock -CommandName Get-MgRoleManagementDirectoryRoleEligibilityScheduleRequest -MockWith {
throw [System.Exception]::new(@'
Status: 403 (Forbidden)
ErrorCode: UnknownError
{"errorCode":"PermissionScopeNotGranted","message":"Authorization failed due to missing permission scopes"}
'@)
}
}

It 'returns no history instead of terminating when PIM history permissions are missing' {
$warnings = $null
$result = Get-MyRoleHistory -DaysBack 7 -WarningVariable warnings -WarningAction SilentlyContinue

@($result).Count | Should -Be 0
($warnings -join ' ') | Should -Match 'Missing Microsoft Graph application permission'
}
}
67 changes: 67 additions & 0 deletions Tests/Invoke-MyGraphBatchResponse.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
BeforeAll {
. (Join-Path $PSScriptRoot '..\Private\Invoke-MyGraphBatchRequest.ps1')
. (Join-Path $PSScriptRoot '..\Private\Invoke-MyGraphBatchResponse.ps1')
}

Describe 'Invoke-MyGraphBatchResponse' {
It 'retries throttled batch subrequests and returns successful results' {
$initialResponse = [PSCustomObject]@{
responses = @(
[PSCustomObject]@{
id = 'summary_0_1'
status = 200
body = [PSCustomObject]@{ value = @('ok') }
},
[PSCustomObject]@{
id = 'summary_0_2'
status = 429
headers = [PSCustomObject]@{ 'Retry-After' = '0' }
body = [PSCustomObject]@{
error = [PSCustomObject]@{
code = 'UnknownError'
message = 'Too Many Requests'
}
}
}
)
}

$retryResponse = [PSCustomObject]@{
responses = @(
[PSCustomObject]@{
id = 'summary_0_2'
status = 200
body = [PSCustomObject]@{ value = @('retried') }
}
)
}

$idMap = @{
summary_0_1 = 'user1'
summary_0_2 = 'user2'
}
$requestsById = @{
summary_0_1 = @{
id = 'summary_0_1'
method = 'GET'
url = '/users/user1/authentication/methods'
}
summary_0_2 = @{
id = 'summary_0_2'
method = 'GET'
url = '/users/user2/authentication/methods'
}
}

Mock -CommandName Start-Sleep -MockWith {}
Mock -CommandName Invoke-MyGraphBatchRequest -MockWith { return $retryResponse }

$results = Invoke-MyGraphBatchResponse -BatchResponses $initialResponse -IdMap $idMap -DataType 'Auth Methods Summary' -RequestsById $requestsById -WarningAction SilentlyContinue

Assert-MockCalled -CommandName Start-Sleep -Times 1 -Exactly
Assert-MockCalled -CommandName Invoke-MyGraphBatchRequest -Times 1 -Exactly
@($results).Count | Should -Be 2
@($results | Where-Object Success).Count | Should -Be 2
@($results | Where-Object { $_.Context -eq 'user2' -and $_.Success }).Count | Should -Be 1
}
}
22 changes: 22 additions & 0 deletions Tests/ModuleManifest.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Describe 'GraphEssentials module manifest' {
BeforeAll {
$testDirectory = Split-Path -Parent $PSCommandPath
$modulePath = Resolve-Path (Join-Path $testDirectory '..\GraphEssentials.psd1')
$moduleManifest = Import-PowerShellDataFile -Path $modulePath
}

It 'defines the expected root module' {
$moduleManifest.RootModule | Should -Be 'GraphEssentials.psm1'
}

It 'exports key public commands' {
$moduleManifest.FunctionsToExport | Should -Contain 'Get-MyRoleHistory'
$moduleManifest.FunctionsToExport | Should -Contain 'Get-MyUserAuthentication'
$moduleManifest.FunctionsToExport | Should -Contain 'Show-MyRole'
}

It 'declares Graph dependencies' {
$moduleManifest.RequiredModules.ModuleName | Should -Contain 'Microsoft.Graph.Authentication'
$moduleManifest.RequiredModules.ModuleName | Should -Contain 'Microsoft.Graph.Identity.Governance'
}
}
Loading