tests/SharingLinkAudit.Tests.ps1
|
#requires -Module Pester BeforeAll { $moduleRoot = Split-Path $PSScriptRoot -Parent Import-Module (Join-Path $moduleRoot 'SharingLinkAudit.psd1') -Force } Describe 'Module surface' { It 'exports exactly the three public functions' { $exported = (Get-Command -Module SharingLinkAudit).Name | Sort-Object $exported | Should -Be @('Export-SharingLinkReport', 'Get-SharingLink', 'Remove-SharingLink') } It 'every public function has comment-based help' { foreach ($fn in 'Get-SharingLink','Remove-SharingLink','Export-SharingLinkReport') { (Get-Help $fn).Synopsis | Should -Not -BeNullOrEmpty } } } Describe 'Get-LinkRisk - the risk ladder' { # Private function, so reach into the module scope to test it directly. It 'rates a specific-person view link as Low' { InModuleScope SharingLinkAudit { Get-LinkRisk -Scope 'Users' -LinkType 'View' -Expiration $null -HasPassword $false | Should -Be 'Low' } } It 'rates an organization view link as Medium' { InModuleScope SharingLinkAudit { Get-LinkRisk -Scope 'Organization' -LinkType 'View' -Expiration $null -HasPassword $false | Should -Be 'Medium' } } It 'rates an organization edit link with no expiry as High' { InModuleScope SharingLinkAudit { Get-LinkRisk -Scope 'Organization' -LinkType 'Edit' -Expiration $null -HasPassword $false | Should -Be 'High' } } It 'rates an anonymous no-password edit link as Critical' { InModuleScope SharingLinkAudit { Get-LinkRisk -Scope 'Anonymous' -LinkType 'Edit' -Expiration $null -HasPassword $false | Should -Be 'Critical' } } It 'an expiry date lowers the risk band of a broad link' { # Anonymous view, no password: crosses Critical -> High once an expiry is set. InModuleScope SharingLinkAudit { $withoutExpiry = Get-LinkRisk -Scope 'Anonymous' -LinkType 'View' -Expiration $null -HasPassword $false $withExpiry = Get-LinkRisk -Scope 'Anonymous' -LinkType 'View' -Expiration (Get-Date) -HasPassword $false $withoutExpiry | Should -Be 'Critical' $withExpiry | Should -Be 'High' } } It 'expiry does not change a specific-person link (it was already low risk)' { InModuleScope SharingLinkAudit { Get-LinkRisk -Scope 'Users' -LinkType 'View' -Expiration $null -HasPassword $false | Should -Be 'Low' Get-LinkRisk -Scope 'Users' -LinkType 'View' -Expiration (Get-Date) -HasPassword $false | Should -Be 'Low' } } It 'a password lowers the risk of an anonymous link' { InModuleScope SharingLinkAudit { $noPw = Get-LinkRisk -Scope 'Anonymous' -LinkType 'View' -Expiration $null -HasPassword $false $pw = Get-LinkRisk -Scope 'Anonymous' -LinkType 'View' -Expiration $null -HasPassword $true # no-password anonymous view = 3+1(noexp)+1(nopw)=5 Critical; with pw = 4 High $noPw | Should -Be 'Critical' $pw | Should -Be 'High' } } } Describe 'Export-SharingLinkReport' { BeforeAll { $script:sample = @( [pscustomobject]@{ SiteUrl='x'; SiteTitle='Sales'; ItemType='File'; ItemName='Budget.xlsx'; Label=$null; Scope='Anonymous'; Access='Edit'; Recipients='Anyone with the link'; Expiration=$null; HasPassword=$false; BlocksDownload=$false; RiskLevel='Critical'; WebUrl='https://x'; LinkId='1' } [pscustomobject]@{ SiteUrl='x'; SiteTitle='HR'; ItemType='File'; ItemName='Pay & Bonus.docx'; Label='Confidential'; Scope='Organization'; Access='View'; Recipients='Everyone'; Expiration=(Get-Date).AddDays(30); HasPassword=$false; BlocksDownload=$true; RiskLevel='Medium'; WebUrl='https://y'; LinkId='2' } ) $script:tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("slatest_" + [System.IO.Path]::GetRandomFileName()) New-Item -ItemType Directory -Path $script:tmp | Out-Null } AfterAll { Remove-Item $script:tmp -Recurse -Force -ErrorAction SilentlyContinue } It 'writes a CSV with one row per link' { $csv = Join-Path $script:tmp 'r.csv' $script:sample | Export-SharingLinkReport -Path $csv (Import-Csv $csv).Count | Should -Be 2 } It 'writes self-contained, well-formed HTML' { $htmlPath = Join-Path $script:tmp 'r.html' $script:sample | Export-SharingLinkReport -Path $htmlPath -Html $content = Get-Content $htmlPath -Raw $content | Should -Match '<!doctype html>' $content.TrimEnd() | Should -Match '</html>$' $content | Should -Not -Match 'src="http' # no external assets } It 'HTML-encodes special characters in item names (no injection)' { $htmlPath = Join-Path $script:tmp 'enc.html' $script:sample | Export-SharingLinkReport -Path $htmlPath -Html $content = Get-Content $htmlPath -Raw $content | Should -Match 'Pay & Bonus' $content | Should -Not -Match 'Pay & Bonus\.docx' # raw & would mean unencoded } It 'warns and writes nothing when there are no links' { $empty = Join-Path $script:tmp 'empty.csv' @() | Export-SharingLinkReport -Path $empty -WarningAction SilentlyContinue Test-Path $empty | Should -BeFalse } } Describe 'Remove-SharingLink guards' { It 'skips objects that lack ItemPath/LinkId' { InModuleScope SharingLinkAudit { Mock Remove-PnPFileSharingLink {} Mock Remove-PnPFolderSharingLink {} [pscustomobject]@{ ItemName = 'junk' } | Remove-SharingLink -Confirm:$false -WarningAction SilentlyContinue Should -Invoke Remove-PnPFileSharingLink -Times 0 } } It '-WhatIf revokes nothing' { InModuleScope SharingLinkAudit { Mock Remove-PnPFileSharingLink {} [pscustomobject]@{ ItemType='File'; ItemName='f'; ItemPath='/x'; LinkId='1'; Scope='Anonymous'; Access='Edit' } | Remove-SharingLink -WhatIf Should -Invoke Remove-PnPFileSharingLink -Times 0 } } It 'calls the file cmdlet for a file link when confirmed' { InModuleScope SharingLinkAudit { Mock Remove-PnPFileSharingLink {} Mock Invoke-WithRetry { & $Action } # run the action inline in tests [pscustomobject]@{ ItemType='File'; ItemName='f'; ItemPath='/x'; LinkId='1'; Scope='Anonymous'; Access='Edit' } | Remove-SharingLink -Confirm:$false Should -Invoke Remove-PnPFileSharingLink -Times 1 } } It 'calls the folder cmdlet for a folder link' { InModuleScope SharingLinkAudit { Mock Remove-PnPFolderSharingLink {} Mock Invoke-WithRetry { & $Action } [pscustomobject]@{ ItemType='Folder'; ItemName='fld'; ItemPath='/x'; LinkId='1'; Scope='Organization'; Access='View' } | Remove-SharingLink -Confirm:$false Should -Invoke Remove-PnPFolderSharingLink -Times 1 } } } Describe 'Get-SharingLink user-facing messages' { It 'says so, professionally, when a site has no links' { InModuleScope SharingLinkAudit { Mock Connect-IfNeeded {} Mock Get-SiteSharingLink { } # no links # capture the information stream (6) $info = Get-SharingLink -SiteUrl 'https://x/sites/Empty' -UseExistingConnection 6>&1 "$info" | Should -Match 'No sharing links found' } } It 'emits link objects (not a message) when links exist' { InModuleScope SharingLinkAudit { Mock Connect-IfNeeded {} Mock Get-SiteSharingLink { [pscustomobject]@{ SiteUrl='x';SiteTitle='S';ItemType='File';ItemName='a';Label=$null;Scope='Anonymous';Access='Edit';Recipients='Anyone';Expiration=$null;HasPassword=$false;BlocksDownload=$false;RiskLevel='Critical';WebUrl='u';LinkId='1' } } $r = Get-SharingLink -SiteUrl 'https://x/sites/Full' -UseExistingConnection $r.Count | Should -Be 1 $r.RiskLevel | Should -Be 'Critical' } } } Describe 'Invoke-WithRetry' { It 'returns the action result on success without retrying' { InModuleScope SharingLinkAudit { $script:calls = 0 $r = Invoke-WithRetry -Action { $script:calls++; 'ok' } $r | Should -Be 'ok' $script:calls | Should -Be 1 } } It 'rethrows a non-throttle error immediately' { InModuleScope SharingLinkAudit { $script:calls = 0 { Invoke-WithRetry -Action { $script:calls++; throw 'File not found' } } | Should -Throw $script:calls | Should -Be 1 # no retry on a non-throttle error } } } |