Skip to content

feat: gate tests on license availability using BeforeDiscovery skip pattern#1727

Closed
Mynster9361 wants to merge 11 commits into
maester365:mainfrom
Mynster9361:license-based-tests
Closed

feat: gate tests on license availability using BeforeDiscovery skip pattern#1727
Mynster9361 wants to merge 11 commits into
maester365:mainfrom
Mynster9361:license-based-tests

Conversation

@Mynster9361
Copy link
Copy Markdown
Contributor

Add BeforeDiscovery { $Licenses = Get-MtSessionLicenses } and Describe-level -Skip expressions to tests that require specific licenses:

  • License-EntraP2: CA risk sign-in/user risk tests (MT.1012, 1013, 1049), PIM alerts tests (MT.1029–1032)
  • License-EntraGovernance: Entitlement Management tests (MT.1106–1110)
  • License-Intune: MDE antivirus and Intune platform tests

Also fix MT.1033/1034 test numbering (array IndexOf on single-object variables) and convert MT.1034 inline license check to -Skip expression.

Co-authored-by: Copilot copilot@github.com

📑 Description

This PR introduces a consistent, discovery-time license gating pattern across Maester test files. Previously, tests that required specific licenses either ran and produced misleading failures on unlicensed tenants, or used inline if/else guards that don't integrate cleanly with Pester's skip mechanism or the new -AutoFilterLicenses tag filtering in Invoke-Maester.

Pattern applied:

BeforeDiscovery {
    $Licenses = Get-MtSessionLicenses
}

Describe "..." -Tag "...", "License-EntraP2" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') {
    It "MT.XXXX: ..." -Tag "MT.XXXX" { ... }
}

Get-MtSessionLicenses returns the license map pre-fetched by Initialize-MtSession at startup — no extra Graph API calls at discovery time.

Tests updated:

License tag Tests
License-EntraP2 MT.1012, MT.1013, MT.1029, MT.1030, MT.1031, MT.1032, MT.1049
License-EntraGovernance MT.1106, MT.1107, MT.1108, MT.1109, MT.1110
License-Intune MT.1053–MT.1060 (Intune platform), MT.1148+ (MDE antivirus)

Additional fixes:

  • MT.1033/1034: $collection.IndexOf($_) on a potentially single-object variable replaced with [array]::IndexOf(@($collection), $_) to avoid calling .IndexOf() on a non-array type.
  • MT.1034: Inline if (Get-MtLicenseInformation) guard removed in favour of -Skip:($EntraIDPlan -eq 'Free') on the It block, consistent with the rest of the file.
  • PIM Describe block (MT.1029–1032): Inline if/else + Add-MtTestResultDetail -SkippedBecause NotLicensedEntraIDP2 replaced with a single Describe-level -Skip, removing duplicated boilerplate across four tests.

Added a new Gating tests on license availability section to advanced-concepts.md covering Get-MtSessionLicenses, the BeforeDiscovery skip pattern, license value and skip expression reference tables, and the License-* tag taxonomy.

Closes #

#1071
#894
#812

✅ Checks

  • My pull request adheres to the code style of this project.
  • My code requires changes to the documentation.
  • I have updated the documentation as required.
  • The build and unit tests pass after running /powershell/tests/pester.ps1 locally.

ℹ️ Additional Information

Compatibility: The -Skip:() expression in BeforeDiscovery scope is evaluated at Pester discovery time. If Get-MtSessionLicenses returns an empty hashtable (e.g. when running tests directly with Invoke-Pester without a Graph connection), $Licenses.EntraID will be $null, which means -Skip:($null -notin 'P2', 'Governance') evaluates to $true — the tests will be skipped rather than fail. This is the safe default for disconnected/offline runs.

Works with -AutoFilterLicenses: The License-* tags added to each Describe block are what Invoke-Maester -AutoFilterLicenses uses to exclude unlicensed tests via ExcludeTag at the Pester filter level — a faster path that skips the tests before discovery rather than during it.


How to Contribute

🏗️ Read our full contributing guide for the Maester project.
🧪 We also have additional instructions and a checklist for creating tests.

Join us at the Maester repository discussions or Entra Discord for more help and conversations!
While you wait for a review, why not spread some Maester love on social media? Thank you! 💖

Mynster9361 and others added 5 commits May 3, 2026 14:58
Add $__MtSession.Licenses hashtable populated once by Initialize-MtSession
at startup. Get-MtLicenseInformation is now cache-first and lazy-loads SKU
data only on a cache miss. Add Get-MtSessionLicenses for use in Pester
BeforeDiscovery blocks to read the cache without additional Graph API calls.
Clear-ModuleVariable resets the cache on each Invoke-Maester run.

Co-authored-by: Copilot <copilot@github.com>
When -AutoFilterLicenses is set, Maester reads the pre-fetched session
license cache and appends the appropriate License-* tags to ExcludeTag
so tests requiring absent licenses are skipped cleanly at discovery time.

Co-authored-by: Copilot <copilot@github.com>
Replace ambiguous -Error parameter with -SkippedBecause Error -SkippedError
in six CA helper catch blocks (was causing InvalidResult in Pester).

Move the $hasRiskCAPolicy skip in Test-MtCaMisconfiguredIDProtection outside
the try-catch to prevent Pester's internal flow-control exception from being
caught and re-thrown as a double Set-ItResult error (MT.1049).

Replace MtRoleDefinition object casts with .ToString() plus stable GUID
fallbacks in Test-MtCaExclusionForDirectorySyncAccount, and add a null guard
to Get-MtRoleInfo for when the module role cache is unavailable (MT.1020).

Co-authored-by: Copilot <copilot@github.com>
…attern

Add BeforeDiscovery { $Licenses = Get-MtSessionLicenses } and Describe-level
-Skip expressions to tests that require specific licenses:

- License-EntraP2: CA risk sign-in/user risk tests (MT.1012, 1013, 1049),
  PIM alerts tests (MT.1029–1032)
- License-EntraGovernance: Entitlement Management tests (MT.1106–1110)
- License-Intune: MDE antivirus and Intune platform tests

Also fix MT.1033/1034 test numbering (array IndexOf on single-object
variables) and convert MT.1034 inline license check to -Skip expression.

Co-authored-by: Copilot <copilot@github.com>
…-MtSessionLicenses

Co-authored-by: Copilot <copilot@github.com>
@Mynster9361 Mynster9361 requested review from a team as code owners May 3, 2026 13:14
@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 3, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

1. Pester test singular noun for Get-MTSessionLicenses -> Get-MTSessionLicens + all references
2. website build error with incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line due to Helper: not being encapsulated
Comment thread powershell/public/Get-MtSessionLicens.ps1 Outdated
@Mynster9361 Mynster9361 mentioned this pull request May 4, 2026
4 tasks
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a discovery-time license-gating pattern so Maester tests can skip cleanly on unlicensed tenants, while also prefetching license data in session state for reuse across test discovery and execution.

Changes:

  • Added BeforeDiscovery + -Skip license gating to selected Entra, Intune, Defender, and Governance test files.
  • Added session-level license caching/accessors and new Invoke-Maester auto-filter logic for License-* tags.
  • Updated docs for license gating and fixed a few related test/runtime edge cases.

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
website/docs/writing-tests/advanced-concepts.md Documents the new license-gating pattern and tag taxonomy.
website/docs/commands/Test-MaesterResultValid.mdx Quotes YAML front matter description.
website/docs/commands/Import-SingleResultFile.mdx Quotes YAML front matter description.
tests/Maester/Intune/Test-MtIntunePlatform.Tests.ps1 Adds Intune license-based Describe skip.
tests/Maester/Entra/Test-PrivilegedAssignments.Tests.ps1 Adds P2/Governance gating to PIM alert tests.
tests/Maester/Entra/Test-MtEntitlementManagementValidResourceRoles.Tests.ps1 Adds Governance license gating.
tests/Maester/Entra/Test-MtEntitlementManagementValidApprovers.Tests.ps1 Adds Governance license gating.
tests/Maester/Entra/Test-MtEntitlementManagementOrphanedResources.Tests.ps1 Adds Governance license gating.
tests/Maester/Entra/Test-MtEntitlementManagementInactivePolicies.Tests.ps1 Adds Governance license gating.
tests/Maester/Entra/Test-MtEntitlementManagementDeletedGroups.Tests.ps1 Adds Governance license gating.
tests/Maester/Entra/Test-ConditionalAccessWhatIf.Tests.ps1 Uses cached license data and fixes IndexOf handling.
tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 Adds cached Entra plan usage and new license tags/skips.
tests/Maester/Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 Adds Intune license gating for MDE policy tests.
powershell/public/maester/entra/Test-MtCaWIFBlockLegacyAuthentication.ps1 Changes error handling to skipped-on-error.
powershell/public/maester/entra/Test-MtCaRequirePasswordChangeForHighUserRisk.ps1 Changes error handling to skipped-on-error.
powershell/public/maester/entra/Test-MtCaReferencedGroupsExist.ps1 Changes error handling to skipped-on-error.
powershell/public/maester/entra/Test-MtCaMisconfiguredIDProtection.ps1 Refactors skip flow around try/catch and result setup.
powershell/public/maester/entra/Test-MtCaMfaForGuest.ps1 Changes error handling to skipped-on-error.
powershell/public/maester/entra/Test-MtCaMfaForAllUsers.ps1 Changes error handling to skipped-on-error.
powershell/public/maester/entra/Test-MtCaExclusionForDirectorySyncAccount.ps1 Adds stable GUID fallback for directory sync roles.
powershell/public/Invoke-Maester.ps1 Adds new auto license-tag exclusion switch and example.
powershell/public/Get-MtSessionLicens.ps1 Adds a new public session-license accessor function.
powershell/public/Get-MtLicenseInformation.ps1 Adds cache-aware license lookup and session writes.
powershell/Maester.psm1 Extends session state with cached license data.
powershell/Maester.psd1 Exports the new public session-license function.
powershell/internal/Initialize-MtSession.ps1 Prefetches license products into session cache.
powershell/internal/Get-MtRoleInfo.ps1 Adds a null guard and reformats role definitions.
powershell/internal/Clear-ModuleVariable.ps1 Clears cached licenses during session reset.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread powershell/public/Get-MtSessionLicense.ps1
Comment thread powershell/public/Invoke-Maester.ps1 Outdated
Comment thread tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1
Comment thread tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1
Comment thread powershell/public/Invoke-Maester.ps1 Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread powershell/public/Get-MtSessionLicense.ps1
Comment thread powershell/public/Invoke-Maester.ps1 Outdated
Comment thread tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1
Comment thread tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1
Comment thread powershell/public/Invoke-Maester.ps1 Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@SamErde
Copy link
Copy Markdown
Contributor

SamErde commented May 4, 2026

@copilot resolve the merge conflicts in this pull request

@Mynster9361
Copy link
Copy Markdown
Contributor Author

@SamErde Got through most of the suggestions/recommendations.

I have not made a unit test for it yet will properly first get arround to it at the earliest wednessday so if you want to give it a go you are welcome to do :)


Write-Information "Total Exchange Online licenses: $TotalLicenses"
return $TotalLicenses
$LicenseType = $TotalLicenses
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Trying to remember how this function works: should $LicenseType be returning the total count of licenses? That sounds like a mismatch, but I could be wrong.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 29 out of 29 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +218 to +223
# Automatically exclude tests that require licenses not present in the tenant.
# When set, Maester queries the tenant license information at startup and adds the
# appropriate License-* tags to ExcludeTag so unlicensed tests are skipped cleanly.
# This requires a Graph connection and is silently ignored when not connected.
[Parameter(HelpMessage = 'Skip tests that require licenses the tenant does not have.')]
[switch] $AutoFilterLicense
# Lazy-load SKU data only when a cache miss requires it.
if ($null -eq $SKUs) {
$SKUs = Invoke-MtGraphRequest -RelativeUri 'subscribedSkus' | Where-Object { $_.capabilityStatus -eq 'Enabled' }
$ServicePlans = $SKUs | Select-Object -ExpandProperty servicePlans | Select-Object -ExpandProperty servicePlanId | Sort-Object -Unique
Comment on lines +344 to +380
# If -AutoFilterLicense is set, exclude tests whose required license is absent in the tenant.
if ($AutoFilterLicense.IsPresent) {
$tenantLicenses = Get-MtSessionLicense
if ($tenantLicenses.Count -gt 0) {
$licenseExclusions = [System.Collections.Generic.List[string]]::new()

# Entra ID tiers: Free excludes P1 + P2 + Governance; P1 excludes P2 + Governance
switch ($tenantLicenses['EntraID']) {
'Free' {
$licenseExclusions.AddRange([string[]]@('License-EntraP1', 'License-EntraP2', 'License-EntraGovernance'))
}
'P1' {
$licenseExclusions.AddRange([string[]]@('License-EntraP2', 'License-EntraGovernance'))
}
}

# Binary (licensed / not licensed) products
$binaryProducts = @{
'EntraWorkloadID' = 'License-EntraWorkloadID'
'Eop' = 'License-Eop'
'ExoDlp' = 'License-ExoDlp'
'Mdo' = 'License-Mdo'
'AdvAudit' = 'License-AdvAudit'
'DefenderXDR' = 'License-DefenderXDR'
'CustomerLockbox' = 'License-CustomerLockbox'
'Intune' = 'License-Intune'
}
foreach ($entry in $binaryProducts.GetEnumerator()) {
if ($null -eq $tenantLicenses[$entry.Key]) {
$licenseExclusions.Add($entry.Value)
}
}

if ($licenseExclusions.Count -gt 0) {
$ExcludeTag = @($ExcludeTag | Where-Object { $_ }) + $licenseExclusions
Write-Verbose "AutoFilterLicense: excluding tags $($licenseExclusions -join ', ')"
}
Context 'Maester/Entra' -ForEach @( $RegularUsers ) {
# Regular users
It "MT.1033.$($RegularUsers.IndexOf($_)): User should be blocked from using legacy authentication ($($_.userPrincipalName))" -Tag 'MT.1033', 'CA', 'CAWhatIf', 'LongRunning', 'Maester' -Skip:( $EntraIDPlan -eq 'Free' ) {
It "MT.1033.$([array]::IndexOf(@($RegularUsers), $_)): User should be blocked from using legacy authentication ($($_.userPrincipalName))" -Tag 'MT.1033', 'CA', 'CAWhatIf', 'LongRunning', 'Maester' -Skip:( $EntraIDPlan -eq 'Free' ) {
} else {
Test-MtConditionalAccessWhatIf -UserId $id -IncludeApplications '00000002-0000-0ff1-ce00-000000000000' -ClientAppType exchangeActiveSync | Should -BeNullOrEmpty
}
It "MT.1034.$([array]::IndexOf(@($EmergencyAccessUsers), $_)): Emergency access users should not be blocked ($($_.userPrincipalName))" -Tag 'MT.1034' -Skip:($EntraIDPlan -eq 'Free') {
@SamErde
Copy link
Copy Markdown
Contributor

SamErde commented May 5, 2026

Amazing PR, @Mynster9361! This has been asked for many times and will make a lot of people happy.
Thank you so much for adding the review, @moorereason!

Just a couple of minor changes requested. This is almost ready to merge!

# appropriate License-* tags to ExcludeTag so unlicensed tests are skipped cleanly.
# This requires a Graph connection and is silently ignored when not connected.
[Parameter(HelpMessage = 'Skip tests that require licenses the tenant does not have.')]
[switch] $AutoFilterLicense
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should automatically exclude tests that the tenant is not licensed for and not require a parameter for this.

The alternative should be opt-in: use a switch (e.g. IgnoreLicense) to perform all tests regardless of what licenses are detected.

Comment on lines +351 to +378
switch ($tenantLicenses['EntraID']) {
'Free' {
$licenseExclusions.AddRange([string[]]@('License-EntraP1', 'License-EntraP2', 'License-EntraGovernance'))
}
'P1' {
$licenseExclusions.AddRange([string[]]@('License-EntraP2', 'License-EntraGovernance'))
}
}

# Binary (licensed / not licensed) products
$binaryProducts = @{
'EntraWorkloadID' = 'License-EntraWorkloadID'
'Eop' = 'License-Eop'
'ExoDlp' = 'License-ExoDlp'
'Mdo' = 'License-Mdo'
'AdvAudit' = 'License-AdvAudit'
'DefenderXDR' = 'License-DefenderXDR'
'CustomerLockbox' = 'License-CustomerLockbox'
'Intune' = 'License-Intune'
}
foreach ($entry in $binaryProducts.GetEnumerator()) {
if ($null -eq $tenantLicenses[$entry.Key]) {
$licenseExclusions.Add($entry.Value)
}
}

if ($licenseExclusions.Count -gt 0) {
$ExcludeTag = @($ExcludeTag | Where-Object { $_ }) + $licenseExclusions
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please align this tag structure with the way that the optional Severity:{Severity} tags are written. For example, License:Intune.

See https://maester.dev/docs/configuration/severity-levels.

Context 'Maester/Entra' -ForEach @( $RegularUsers ) {
# Regular users
It "MT.1033.$($RegularUsers.IndexOf($_)): User should be blocked from using legacy authentication ($($_.userPrincipalName))" -Tag 'MT.1033', 'CA', 'CAWhatIf', 'LongRunning', 'Maester' -Skip:( $EntraIDPlan -eq 'Free' ) {
It "MT.1033.$([array]::IndexOf(@($RegularUsers), $_)): User should be blocked from using legacy authentication ($($_.userPrincipalName))" -Tag 'MT.1033', 'CA', 'CAWhatIf', 'LongRunning', 'Maester' -Skip:( $EntraIDPlan -eq 'Free' ) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice cleanup! 👍 For future reference, it is a good practice to use separate PRs for different types of changes/fixes so they can all be reviewed separately.

@merill
Copy link
Copy Markdown
Contributor

merill commented May 7, 2026

@Mynster9361 Thanks for this. Please see my comment at #1746 (comment)

This is something I've been working on at Microsoft on the ZTAssessment. My plan was to bring that implementation over here since it solves some of the key problems in how we add tags for our current tests.

@Mynster9361
Copy link
Copy Markdown
Contributor Author

@Mynster9361 Thanks for this. Please see my comment at #1746 (comment)

This is something I've been working on at Microsoft on the ZTAssessment. My plan was to bring that implementation over here since it solves some of the key problems in how we add tags for our current tests.

That is awesome @merill does that mean we should close this one and keep the work going in #1746 @SamErde ?

@merill
Copy link
Copy Markdown
Contributor

merill commented May 7, 2026

Yeah, I think we can merge into that one. There is a bit of work to do though including backfilling the licensing for all the existing tests.

@Mynster9361
Copy link
Copy Markdown
Contributor Author

definently just keeping the PR open till i have some time to write down the tests i included in this one in regards to license requirements and will add that to #1746

@Mynster9361
Copy link
Copy Markdown
Contributor Author

closing this PR have added the license information from this pr to #1746

@Mynster9361 Mynster9361 closed this May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🪲 New MT Test 1106-9 Fail due to missing license entitlements

5 participants