Tests/Unit/LicenseManager.Tests.ps1
|
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } BeforeAll { . (Join-Path $PSScriptRoot '../../Private/LicenseManager.ps1') . (Join-Path $PSScriptRoot '../../Public/License/Get-SPCLicenseInfo.ps1') . (Join-Path $PSScriptRoot '../../Public/License/Register-SPCLicense.ps1') $script:TestUUID = '6F0E4C97-B72A-4E69-A11B-F6C4AF6517E7' $script:TestEmail = 'test@contoso.com' function New-TestLicFile { param( [string]$Path, [string]$Tier = 'PRO', [int]$AgeDays = 0, [bool]$IsTesting = $false ) $lastVerifiedAt = [datetime]::UtcNow.AddDays(-$AgeDays).ToString('o') @{ licenseKey = $script:TestUUID tier = $Tier email = $script:TestEmail isTesting = $IsTesting registeredAt = [datetime]::UtcNow.AddDays(-30).ToString('o') lastVerifiedAt = $lastVerifiedAt } | ConvertTo-Json | Set-Content -Path $Path -Encoding UTF8 } function New-ValidApiResponse { param( [string]$Tier = 'PRO', [bool]$Refunded = $false, [bool]$Testing = $false ) [PSCustomObject]@{ success = $true uses = 1 purchase = [PSCustomObject]@{ email = $script:TestEmail product_name = "SPClean $Tier" refunded = $Refunded chargebacked = $false test = $Testing } } } } # --------------------------------------------------------------------------- # Invoke-GumroadVerifyInternal # --------------------------------------------------------------------------- Describe 'Invoke-GumroadVerifyInternal' { It 'returns Success=true with email for a valid PRO key' { Mock Invoke-RestMethod { New-ValidApiResponse -Tier 'PRO' } ` -ParameterFilter { $Uri -like '*gumroad*' } $r = Invoke-GumroadVerifyInternal -LicenseKey $script:TestUUID -Tier 'PRO' $r.Success | Should -BeTrue $r.Email | Should -Be $script:TestEmail $r.Tier | Should -Be 'PRO' } It 'returns Success=false (not throw) on HTTP 404' { Mock Invoke-RestMethod { $msg = [System.Net.Http.HttpResponseMessage]::new( [System.Net.HttpStatusCode]::NotFound) throw [Microsoft.PowerShell.Commands.HttpResponseException]::new('Not Found', $msg) } -ParameterFilter { $Uri -like '*gumroad*' } $r = Invoke-GumroadVerifyInternal -LicenseKey $script:TestUUID -Tier 'PRO' $r.Success | Should -BeFalse } It 'throws ERR-LIC-NET-001 on network error' { Mock Invoke-RestMethod { throw [System.Net.Http.HttpRequestException]::new('Connection refused') } -ParameterFilter { $Uri -like '*gumroad*' } { Invoke-GumroadVerifyInternal -LicenseKey $script:TestUUID -Tier 'PRO' } | Should -Throw '*ERR-LIC-NET-001*' } It 'returns Refunded=true when purchase.refunded is true' { Mock Invoke-RestMethod { New-ValidApiResponse -Refunded $true } ` -ParameterFilter { $Uri -like '*gumroad*' } $r = Invoke-GumroadVerifyInternal -LicenseKey $script:TestUUID -Tier 'PRO' $r.Refunded | Should -BeTrue } It 'returns IsTesting=true for test purchases' { Mock Invoke-RestMethod { New-ValidApiResponse -Testing $true } ` -ParameterFilter { $Uri -like '*gumroad*' } $r = Invoke-GumroadVerifyInternal -LicenseKey $script:TestUUID -Tier 'PRO' $r.IsTesting | Should -BeTrue } It 'passes increment_uses_count=false when IncrementUses is false' { $script:capturedBody = $null Mock Invoke-RestMethod { $script:capturedBody = $Body New-ValidApiResponse } -ParameterFilter { $Uri -like '*gumroad*' } Invoke-GumroadVerifyInternal -LicenseKey $script:TestUUID -Tier 'PRO' -IncrementUses $false $script:capturedBody.increment_uses_count | Should -Be 'false' } } # --------------------------------------------------------------------------- # Test-SPCLicenseKeyInternal # --------------------------------------------------------------------------- Describe 'Test-SPCLicenseKeyInternal' { It 'returns IsValid=false / MalformedFormat for non-UUID input' { $r = Test-SPCLicenseKeyInternal -LicenseKey 'not-a-uuid' $r.IsValid | Should -BeFalse $r.FailureReason | Should -Be 'MalformedFormat' } It 'trims whitespace before UUID check' { Mock Invoke-RestMethod { New-ValidApiResponse } ` -ParameterFilter { $Uri -like '*gumroad*' } $r = Test-SPCLicenseKeyInternal -LicenseKey " $script:TestUUID " $r.IsValid | Should -BeTrue } It 'returns IsValid=true / Tier=PRO for a valid PRO UUID' { Mock Invoke-RestMethod { New-ValidApiResponse -Tier 'PRO' } ` -ParameterFilter { $Uri -like '*gumroad*' } $r = Test-SPCLicenseKeyInternal -LicenseKey $script:TestUUID $r.IsValid | Should -BeTrue $r.Tier | Should -Be 'PRO' } It 'falls back to CONSULTANT when PRO returns 404' { $script:gumroadCallN = 0 Mock Invoke-RestMethod { $script:gumroadCallN++ if ($script:gumroadCallN -eq 1) { $msg = [System.Net.Http.HttpResponseMessage]::new( [System.Net.HttpStatusCode]::NotFound) throw [Microsoft.PowerShell.Commands.HttpResponseException]::new('Not Found', $msg) } New-ValidApiResponse -Tier 'CONSULTANT' } -ParameterFilter { $Uri -like '*gumroad*' } $r = Test-SPCLicenseKeyInternal -LicenseKey $script:TestUUID $r.IsValid | Should -BeTrue $r.Tier | Should -Be 'CONSULTANT' } It 'returns InvalidKey when both tiers return 404' { Mock Invoke-RestMethod { $msg = [System.Net.Http.HttpResponseMessage]::new( [System.Net.HttpStatusCode]::NotFound) throw [Microsoft.PowerShell.Commands.HttpResponseException]::new('Not Found', $msg) } -ParameterFilter { $Uri -like '*gumroad*' } $r = Test-SPCLicenseKeyInternal -LicenseKey $script:TestUUID $r.IsValid | Should -BeFalse $r.FailureReason | Should -Be 'InvalidKey' } It 'returns NetworkError when PRO call throws non-404' { Mock Invoke-RestMethod { throw [System.Net.Http.HttpRequestException]::new('timeout') } -ParameterFilter { $Uri -like '*gumroad*' } $r = Test-SPCLicenseKeyInternal -LicenseKey $script:TestUUID $r.IsValid | Should -BeFalse $r.FailureReason | Should -Be 'NetworkError' } It 'returns Refunded when key is valid but purchase is refunded' { Mock Invoke-RestMethod { New-ValidApiResponse -Refunded $true } ` -ParameterFilter { $Uri -like '*gumroad*' } $r = Test-SPCLicenseKeyInternal -LicenseKey $script:TestUUID $r.IsValid | Should -BeFalse $r.FailureReason | Should -Be 'Refunded' } It 'propagates IsTesting=true from API response' { Mock Invoke-RestMethod { New-ValidApiResponse -Testing $true } ` -ParameterFilter { $Uri -like '*gumroad*' } $r = Test-SPCLicenseKeyInternal -LicenseKey $script:TestUUID $r.IsTesting | Should -BeTrue } } # --------------------------------------------------------------------------- # Get-SPCLicenseInfo # --------------------------------------------------------------------------- Describe 'Get-SPCLicenseInfo' { BeforeEach { $script:SPCLicenseCache = $null $script:LicenseFilePath = Join-Path $TestDrive 'license.lic' Remove-Item -Path $script:LicenseFilePath -ErrorAction SilentlyContinue } It 'returns Status=Unlicensed when no license file exists' { $r = Get-SPCLicenseInfo $r.Status | Should -Be 'Unlicensed' } It 'returns Status=Active when fresh license file exists' { New-TestLicFile -Path $script:LicenseFilePath -AgeDays 0 $r = Get-SPCLicenseInfo $r.Status | Should -Be 'Active' $r.Tier | Should -Be 'PRO' $r.Email | Should -Be $script:TestEmail } It 'returns SPC.LicenseInfo TypeName' { New-TestLicFile -Path $script:LicenseFilePath -AgeDays 0 $r = Get-SPCLicenseInfo $r.PSObject.TypeNames | Should -Contain 'SPC.LicenseInfo' } It 'returns cached result on second call without re-reading disk' { New-TestLicFile -Path $script:LicenseFilePath -AgeDays 0 $null = Get-SPCLicenseInfo Remove-Item $script:LicenseFilePath -Force $r = Get-SPCLicenseInfo $r.Status | Should -Be 'Active' } It 'returns Status=Invalid for malformed license file' { Set-Content -Path $script:LicenseFilePath -Value 'not-json' -Encoding UTF8 $r = Get-SPCLicenseInfo $r.Status | Should -Be 'Invalid' } It 'returns Status=Invalid when required fields are missing' { '{"licenseKey":"abc"}' | Set-Content -Path $script:LicenseFilePath -Encoding UTF8 $r = Get-SPCLicenseInfo $r.Status | Should -Be 'Invalid' } It 're-verifies via Gumroad when cache is >= 7 days old and returns Active' { New-TestLicFile -Path $script:LicenseFilePath -AgeDays 8 Mock Invoke-RestMethod { New-ValidApiResponse } ` -ParameterFilter { $Uri -like '*gumroad*' } $r = Get-SPCLicenseInfo $r.Status | Should -Be 'Active' } It 'returns Status=Revoked when re-verify returns refunded=true' { New-TestLicFile -Path $script:LicenseFilePath -AgeDays 8 Mock Invoke-RestMethod { New-ValidApiResponse -Refunded $true } ` -ParameterFilter { $Uri -like '*gumroad*' } $r = Get-SPCLicenseInfo $r.Status | Should -Be 'Revoked' (Test-Path $script:LicenseFilePath) | Should -BeFalse } It 'does not throw when re-verify fails (offline grace)' { New-TestLicFile -Path $script:LicenseFilePath -AgeDays 8 Mock Invoke-RestMethod { throw [System.Net.Http.HttpRequestException]::new('offline') } -ParameterFilter { $Uri -like '*gumroad*' } { Get-SPCLicenseInfo } | Should -Not -Throw } It 'returns Active with offline grace when re-verify fails' { New-TestLicFile -Path $script:LicenseFilePath -AgeDays 8 Mock Invoke-RestMethod { throw [System.Net.Http.HttpRequestException]::new('offline') } -ParameterFilter { $Uri -like '*gumroad*' } $r = Get-SPCLicenseInfo $r.Status | Should -Be 'Active' } It 'returns IsTesting=true when license file has isTesting=true' { New-TestLicFile -Path $script:LicenseFilePath -AgeDays 0 -IsTesting $true $r = Get-SPCLicenseInfo $r.IsTesting | Should -BeTrue } It 'never throws — returns non-Active for nonexistent directory' { $script:LicenseFilePath = Join-Path $TestDrive 'nonexistent\license.lic' { Get-SPCLicenseInfo } | Should -Not -Throw } } # --------------------------------------------------------------------------- # Register-SPCLicense # --------------------------------------------------------------------------- Describe 'Register-SPCLicense' { BeforeEach { $script:SPCLicenseCache = $null $script:LicenseFilePath = Join-Path $TestDrive 'license.lic' Remove-Item -Path $script:LicenseFilePath -ErrorAction SilentlyContinue } It 'writes license.lic and returns SPC.LicenseInfo on success' { Mock Invoke-RestMethod { New-ValidApiResponse -Tier 'PRO' } ` -ParameterFilter { $Uri -like '*gumroad*' } $r = Register-SPCLicense -LicenseKey $script:TestUUID -Confirm:$false $r.Status | Should -Be 'Active' $r.Tier | Should -Be 'PRO' $r.PSObject.TypeNames | Should -Contain 'SPC.LicenseInfo' (Test-Path $script:LicenseFilePath) | Should -BeTrue } It 'license.lic contains expected fields' { Mock Invoke-RestMethod { New-ValidApiResponse -Tier 'PRO' } ` -ParameterFilter { $Uri -like '*gumroad*' } Register-SPCLicense -LicenseKey $script:TestUUID -Confirm:$false $data = Get-Content $script:LicenseFilePath -Raw | ConvertFrom-Json $data.licenseKey | Should -Be $script:TestUUID $data.tier | Should -Be 'PRO' $data.email | Should -Be $script:TestEmail $data.lastVerifiedAt | Should -Not -BeNullOrEmpty $data.registeredAt | Should -Not -BeNullOrEmpty } It 'throws ERR-LIC-001 for an invalid key (404 from API)' { Mock Invoke-RestMethod { $msg = [System.Net.Http.HttpResponseMessage]::new( [System.Net.HttpStatusCode]::NotFound) throw [Microsoft.PowerShell.Commands.HttpResponseException]::new('Not Found', $msg) } -ParameterFilter { $Uri -like '*gumroad*' } { Register-SPCLicense -LicenseKey $script:TestUUID -Confirm:$false } | Should -Throw '*ERR-LIC-001*' } It 'throws ERR-LIC-001 for a malformed (non-UUID) key' { { Register-SPCLicense -LicenseKey 'bad-key' -Confirm:$false } | Should -Throw '*ERR-LIC-001*' } It 'throws ERR-LIC-NET-001 on network error' { Mock Invoke-RestMethod { throw [System.Net.Http.HttpRequestException]::new('no network') } -ParameterFilter { $Uri -like '*gumroad*' } { Register-SPCLicense -LicenseKey $script:TestUUID -Confirm:$false } | Should -Throw '*ERR-LIC-NET-001*' } It 'overwrites existing license when -Force is specified' { New-TestLicFile -Path $script:LicenseFilePath -Tier 'PRO' -AgeDays 0 Mock Invoke-RestMethod { New-ValidApiResponse -Tier 'PRO' } ` -ParameterFilter { $Uri -like '*gumroad*' } $r = Register-SPCLicense -LicenseKey $script:TestUUID -Force -Confirm:$false $r.Status | Should -Be 'Active' (Test-Path $script:LicenseFilePath) | Should -BeTrue } It 'clears SPCLicenseCache after registration' { $script:SPCLicenseCache = [PSCustomObject]@{ Status = 'Active' } Mock Invoke-RestMethod { New-ValidApiResponse } ` -ParameterFilter { $Uri -like '*gumroad*' } Register-SPCLicense -LicenseKey $script:TestUUID -Confirm:$false $script:SPCLicenseCache | Should -BeNullOrEmpty } It 'emits warning for test purchase' { Mock Invoke-RestMethod { New-ValidApiResponse -Testing $true } ` -ParameterFilter { $Uri -like '*gumroad*' } Register-SPCLicense -LicenseKey $script:TestUUID -Confirm:$false ` -WarningVariable warnVar -WarningAction SilentlyContinue $warnVar | Should -Not -BeNullOrEmpty $warnVar[0].Message | Should -BeLike '*Test purchase*' } It 'AC-12: license key does not appear in any output stream' { Mock Invoke-RestMethod { New-ValidApiResponse } ` -ParameterFilter { $Uri -like '*gumroad*' } $allOutput = Register-SPCLicense -LicenseKey $script:TestUUID -Confirm:$false ` -Verbose -WarningVariable w -InformationVariable i 4>&1 5>&1 6>&1 $allOutput | ForEach-Object { "$_" | Should -Not -BeLike "*$script:TestUUID*" } $w | ForEach-Object { "$_" | Should -Not -BeLike "*$script:TestUUID*" } $i | ForEach-Object { "$_" | Should -Not -BeLike "*$script:TestUUID*" } } } # --------------------------------------------------------------------------- # Assert-SPCProLicense # --------------------------------------------------------------------------- Describe 'Assert-SPCProLicense' { BeforeEach { $script:SPCLicenseCache = $null $script:LicenseFilePath = Join-Path $TestDrive 'license.lic' Remove-Item -Path $script:LicenseFilePath -ErrorAction SilentlyContinue } It 'does not throw when Status=Active Tier=PRO' { New-TestLicFile -Path $script:LicenseFilePath -Tier 'PRO' -AgeDays 0 { Assert-SPCProLicense -Feature 'TestFeature' } | Should -Not -Throw } It 'does not throw when Status=Active Tier=CONSULTANT' { New-TestLicFile -Path $script:LicenseFilePath -Tier 'CONSULTANT' -AgeDays 0 { Assert-SPCProLicense -Feature 'TestFeature' } | Should -Not -Throw } It 'throws ERR-LIC-003 when unlicensed' { { Assert-SPCProLicense -Feature 'TestFeature' } | Should -Throw '*ERR-LIC-003*' } It 'throws ERR-LIC-003 when Status=Invalid' { Set-Content -Path $script:LicenseFilePath -Value 'bad' -Encoding UTF8 { Assert-SPCProLicense -Feature 'TestFeature' } | Should -Throw '*ERR-LIC-003*' } } # --------------------------------------------------------------------------- # Assert-SPCConsultantLicense # --------------------------------------------------------------------------- Describe 'Assert-SPCConsultantLicense' { BeforeEach { $script:SPCLicenseCache = $null $script:LicenseFilePath = Join-Path $TestDrive 'license.lic' Remove-Item -Path $script:LicenseFilePath -ErrorAction SilentlyContinue } It 'does not throw when Status=Active Tier=CONSULTANT' { New-TestLicFile -Path $script:LicenseFilePath -Tier 'CONSULTANT' -AgeDays 0 { Assert-SPCConsultantLicense -Feature 'TestFeature' } | Should -Not -Throw } It 'throws ERR-LIC-004 when Tier=PRO' { New-TestLicFile -Path $script:LicenseFilePath -Tier 'PRO' -AgeDays 0 { Assert-SPCConsultantLicense -Feature 'TestFeature' } | Should -Throw '*ERR-LIC-004*' } It 'throws ERR-LIC-004 when unlicensed' { { Assert-SPCConsultantLicense -Feature 'TestFeature' } | Should -Throw '*ERR-LIC-004*' } } # --------------------------------------------------------------------------- # Feature Gate Integration # --------------------------------------------------------------------------- Describe 'Feature Gate Integration' { BeforeAll { . (Join-Path $PSScriptRoot '../../Private/Test-SPCConnection.ps1') . (Join-Path $PSScriptRoot '../../Public/Report/Export-SPCReport.ps1') . (Join-Path $PSScriptRoot '../../Public/Schedule/New-SPCScanSchedule.ps1') . (Join-Path $PSScriptRoot '../../Public/Remediate/Remove-SPCOrphanedUser.ps1') . (Join-Path $PSScriptRoot '../../Public/Remediate/Restore-SPCOrphanedUser.ps1') } BeforeEach { $script:SPCLicenseCache = $null $script:LicenseFilePath = Join-Path $TestDrive 'license.lic' Remove-Item -Path $script:LicenseFilePath -ErrorAction SilentlyContinue Mock Test-SPCConnection {} Mock Connect-PnPOnline {} Mock Disconnect-PnPOnline {} } It 'Export-SPCReport HTML requires Pro — throws ERR-LIC-003 when unlicensed' { $fakeItem = [PSCustomObject]@{ LoginName = 'user@contoso.com'; SiteUrl = 'https://x.sharepoint.com' } { Export-SPCReport -InputObject $fakeItem -Format HTML ` -OutputPath (Join-Path $TestDrive 'report.html') } | Should -Throw '*ERR-LIC-003*' } It 'New-SPCScanSchedule requires Pro — throws ERR-LIC-003 when unlicensed' { $secPwd = ConvertTo-SecureString 'x' -AsPlainText -Force { New-SPCScanSchedule -TenantName 'contoso' -ClientId 'abc' ` -CertificatePath 'C:\fake.pfx' ` -CertificatePassword $secPwd ` -Schedule Daily ` -ReportOutputPath (Join-Path $TestDrive 'reports') ` -Confirm:$false } | Should -Throw '*ERR-LIC-003*' } It 'Remove-SPCOrphanedUser -CreateSnapshot requires Pro — throws ERR-LIC-003 when unlicensed' { $fakeUser = [PSCustomObject]@{ LoginName = 'i:0#.f|membership|user@contoso.com' SiteUrl = 'https://contoso.sharepoint.com/sites/test' DisplayName = 'Test User' } $fakeUser.PSObject.TypeNames.Insert(0, 'SPC.OrphanedUser') { Remove-SPCOrphanedUser -InputObject $fakeUser -CreateSnapshot -Confirm:$false } | Should -Throw '*ERR-LIC-003*' } It 'Restore-SPCOrphanedUser requires Pro — throws ERR-LIC-003 when unlicensed' { { Restore-SPCOrphanedUser -SnapshotPath (Join-Path $TestDrive 'snap.json') -Confirm:$false } | Should -Throw '*ERR-LIC-003*' } } |