diff --git a/.github/PSModule.yml b/.github/PSModule.yml index 60d3f27..d640592 100644 --- a/.github/PSModule.yml +++ b/.github/PSModule.yml @@ -24,7 +24,6 @@ Linter: env: VALIDATE_BIOME_FORMAT: false VALIDATE_BIOME_LINT: false - VALIDATE_GITHUB_ACTIONS_ZIZMOR: false VALIDATE_JSCPD: false VALIDATE_JSON_PRETTIER: false VALIDATE_MARKDOWN_PRETTIER: false diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 53188fe..f25754b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,3 +12,5 @@ updates: - github-actions schedule: interval: weekly + cooldown: + default-days: 7 diff --git a/.github/linters/zizmor.yaml b/.github/linters/zizmor.yaml new file mode 100644 index 0000000..9174feb --- /dev/null +++ b/.github/linters/zizmor.yaml @@ -0,0 +1,18 @@ +--- +# Configuration for zizmor, the GitHub Actions static analysis tool. +# Reference: https://docs.zizmor.sh/configuration/ +# +# Super-linter passes this file with '--config', resolved from +# LINTER_RULES_PATH (default '.github/linters'), so the filename must stay +# 'zizmor.yaml'. + +rules: + unpinned-uses: + config: + # Process-PSModule is a first-party PSModule workflow that this + # repository tracks by major tag on purpose, so the framework can ship + # fixes without a Dependabot bump per consumer. Allow a symbolic ref for + # the PSModule org only; everything else falls through to the implicit + # '*': hash-pin rule. + policies: + PSModule/*: ref-pin diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml index 08fc9a3..1181c77 100644 --- a/.github/workflows/Process-PSModule.yml +++ b/.github/workflows/Process-PSModule.yml @@ -4,6 +4,9 @@ on: workflow_dispatch: schedule: - cron: '0 0 * * *' + push: + branches: + - main pull_request: branches: - main @@ -13,20 +16,21 @@ on: - reopened - synchronize - labeled + - unlabeled concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: false permissions: - contents: write - pull-requests: write - statuses: write + contents: read pages: write id-token: write jobs: Process-PSModule: - uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@bf67cd90269ca5ce25cd76b203678907dc2984b4 # v6.1.19 + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v8 secrets: - APIKey: ${{ secrets.APIKey }} + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + GitHubAppClientId: ${{ secrets.SHELLY_CLIENT_ID }} + GitHubAppPrivateKey: ${{ secrets.SHELLY_PRIVATE_KEY }} diff --git a/tests/NerdFonts.Tests.ps1 b/tests/NerdFonts.Tests.ps1 index 515e3c7..9550db6 100644 --- a/tests/NerdFonts.Tests.ps1 +++ b/tests/NerdFonts.Tests.ps1 @@ -1,4 +1,4 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.1.0'; MaximumVersion = '6.*' } [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseDeclaredVarsMoreThanAssignments', '', @@ -19,129 +19,158 @@ [CmdletBinding()] param() +BeforeAll { + function Use-TestFontData { + <# + .SYNOPSIS + Runs a test body against a replaced module-internal font catalog. + + .DESCRIPTION + Swaps $script:NerdFonts inside the NerdFonts module for the supplied font objects, runs the + body, and restores the original catalog afterwards even when the body fails. + + .EXAMPLE + Use-TestFontData -Fonts $testFonts -Body { Install-NerdFont -Name 'Test' } + + Installs against the test catalog and restores the real catalog when finished. + #> + param( + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [object[]] $Fonts, + + [Parameter(Mandatory)] + [scriptblock] $Body + ) + + $originalFonts = InModuleScope NerdFonts { $script:NerdFonts } + InModuleScope NerdFonts -Parameters @{ fonts = $Fonts } { + param($fonts) + $script:NerdFonts = $fonts + } + try { + & $Body + } finally { + InModuleScope NerdFonts -Parameters @{ fonts = $originalFonts } { + param($fonts) + $script:NerdFonts = $fonts + } + } + } + + function Get-TestFont { + <# + .SYNOPSIS + Gets a single font entry from the repository's FontsData.json. + + .DESCRIPTION + Reads the source FontsData.json and returns the first entry whose name matches exactly, so + tests can build catalogs from real font metadata instead of hard-coded URLs. + + .EXAMPLE + Get-TestFont -Name 'Tinos' + + Returns the Tinos font entry. + #> + param( + [Parameter(Mandatory)] + [string] $Name + ) + + $fontsDataPath = Join-Path -Path $PSScriptRoot -ChildPath '../src/FontsData.json' + Get-Content -Path $fontsDataPath | ConvertFrom-Json | Where-Object Name -EQ $Name | Select-Object -First 1 + } + + function Get-TestCacheRoot { + <# + .SYNOPSIS + Gets the platform-specific NerdFonts download cache root. + + .DESCRIPTION + Mirrors the cache root that Install-NerdFont computes, so tests can seed and clean up cache + entries on the same path the function uses. + + .EXAMPLE + Get-TestCacheRoot + + Returns the cache root path for the current platform. + #> + if ($IsWindows) { + Join-Path -Path ([Environment]::GetFolderPath('LocalApplicationData')) -ChildPath 'PSModule/NerdFonts/cache' + } else { + Join-Path -Path $HOME -ChildPath '.cache/PSModule/NerdFonts' + } + } +} + Describe 'Module' { Context 'Function: Get-NerdFont' { It 'Returns all fonts' { $fonts = Get-NerdFont Write-Verbose ($fonts | Out-String) -Verbose - $fonts | Should -Not -BeNullOrEmpty + $fonts | Should-NotBeNull } It 'Returns a specific font' { $font = Get-NerdFont -Name 'Tinos' Write-Verbose ($font | Out-String) -Verbose - $font | Should -Not -BeNullOrEmpty + $font | Should-NotBeNull + $font.Name | Should-Be 'Tinos' } } Context 'Function: Install-NerdFont' { It 'Install-NerdFont - Installs a font' { - { Install-NerdFont -Name 'Tinos' } | Should -Not -Throw - Get-Font -Name 'Tinos*' | Should -Not -BeNullOrEmpty + Install-NerdFont -Name 'Tinos' + Get-Font -Name 'Tinos*' | Should-NotBeNull } It 'Install-NerdFont - Continues when one queued download fails' { - $originalFonts = InModuleScope NerdFonts { $script:NerdFonts } - $loadedFonts = Get-Content -Path (Join-Path -Path $PSScriptRoot -ChildPath '../src/FontsData.json') | ConvertFrom-Json - $goodFont = $loadedFonts | Where-Object Name -EQ 'Tinos' | Select-Object -First 1 - $testFonts = @( [pscustomobject]@{ Name = 'BrokenDownloadTest' URL = 'https://github.com/ryanoasis/nerd-fonts/releases/download/v3.4.0/does-not-exist.zip' - }, - $goodFont + } + Get-TestFont -Name 'Tinos' ) - InModuleScope NerdFonts -Parameters @{ fonts = $testFonts } { - param($fonts) - $script:NerdFonts = $fonts - } - try { + Use-TestFontData -Fonts $testFonts -Body { Mock -ModuleName NerdFonts Install-Font {} - { Install-NerdFont -Name @('BrokenDownloadTest', 'Tinos') -Force -ErrorAction SilentlyContinue } | Should -Not -Throw - Should -Invoke -ModuleName NerdFonts Install-Font -Times 1 -Exactly - } finally { - InModuleScope NerdFonts -Parameters @{ fonts = $originalFonts } { - param($fonts) - $script:NerdFonts = $fonts - } + + Install-NerdFont -Name @('BrokenDownloadTest', 'Tinos') -Force -ErrorAction SilentlyContinue + + Should-Invoke -CommandName Install-Font -ModuleName NerdFonts -Times 1 -Exactly } } It 'Install-NerdFont - Skips already installed fonts without downloading' { - $originalFonts = InModuleScope NerdFonts { $script:NerdFonts } $testFonts = @( [pscustomobject]@{ Name = 'AlreadyInstalledTest' URL = 'https://example.invalid/already-installed.zip' } ) - InModuleScope NerdFonts -Parameters @{ fonts = $testFonts } { - param($fonts) - $script:NerdFonts = $fonts - } - try { + Use-TestFontData -Fonts $testFonts -Body { Mock -ModuleName NerdFonts Get-Font { [pscustomobject]@{ Name = 'AlreadyInstalledTest Nerd Font' } } Mock -ModuleName NerdFonts Install-Font {} - { Install-NerdFont -Name 'AlreadyInstalledTest' -ErrorAction Stop } | Should -Not -Throw - Should -Invoke -ModuleName NerdFonts Install-Font -Times 0 -Exactly - } finally { - InModuleScope NerdFonts -Parameters @{ fonts = $originalFonts } { - param($fonts) - $script:NerdFonts = $fonts - } - } - } - - It 'Install-NerdFont - Installs a font with -Variant Mono' { - $originalFonts = InModuleScope NerdFonts { $script:NerdFonts } - $loadedFonts = Get-Content -Path (Join-Path -Path $PSScriptRoot -ChildPath '../src/FontsData.json') | ConvertFrom-Json - $goodFont = $loadedFonts | Where-Object Name -EQ 'Hack' | Select-Object -First 1 - $testFonts = @($goodFont) - InModuleScope NerdFonts -Parameters @{ fonts = $testFonts } { - param($fonts) - $script:NerdFonts = $fonts - } - - try { - Mock -ModuleName NerdFonts Get-Font { @() } - $script:TestCapturedFiles = $null - Mock -ModuleName NerdFonts Install-Font {} -ParameterFilter { - $script:TestCapturedFiles = @( - Get-ChildItem -Path $Path -Recurse -File -Include '*.ttf', '*.otf' | - Select-Object -ExpandProperty Name - ) - $true - } + Install-NerdFont -Name 'AlreadyInstalledTest' -ErrorAction Stop - { Install-NerdFont -Name 'Hack' -Variant Mono -ErrorAction Stop } | Should -Not -Throw - Should -Invoke -ModuleName NerdFonts Install-Font -Times 1 -Exactly - $script:TestCapturedFiles | Should -Not -BeNullOrEmpty - $script:TestCapturedFiles | ForEach-Object { $_ | Should -BeLike '*NerdFontMono*' } - } finally { - InModuleScope NerdFonts -Parameters @{ fonts = $originalFonts } { - param($fonts) - $script:NerdFonts = $fonts - } + Should-NotInvoke -CommandName Install-Font -ModuleName NerdFonts } } - It 'Install-NerdFont - Installs a font with -Variant Standard' { - $originalFonts = InModuleScope NerdFonts { $script:NerdFonts } - $loadedFonts = Get-Content -Path (Join-Path -Path $PSScriptRoot -ChildPath '../src/FontsData.json') | ConvertFrom-Json - $goodFont = $loadedFonts | Where-Object Name -EQ 'Hack' | Select-Object -First 1 - $testFonts = @($goodFont) - InModuleScope NerdFonts -Parameters @{ fonts = $testFonts } { - param($fonts) - $script:NerdFonts = $fonts - } + It 'Install-NerdFont - Installs a font with -Variant ' -ForEach @( + @{ Variant = 'Mono'; Expected = '*NerdFontMono*'; NotExpected = @() } + @{ Variant = 'Propo'; Expected = '*NerdFontPropo*'; NotExpected = @() } + @{ Variant = 'Standard'; Expected = '*NerdFont*'; NotExpected = @('*NerdFontMono*', '*NerdFontPropo*') } + ) { + $testFonts = @(Get-TestFont -Name 'Hack') - try { + Use-TestFontData -Fonts $testFonts -Body { Mock -ModuleName NerdFonts Get-Font { @() } $script:TestCapturedFiles = $null Mock -ModuleName NerdFonts Install-Font {} -ParameterFilter { @@ -152,103 +181,51 @@ Describe 'Module' { $true } - { Install-NerdFont -Name 'Hack' -Variant Standard -ErrorAction Stop } | Should -Not -Throw - Should -Invoke -ModuleName NerdFonts Install-Font -Times 1 -Exactly - $script:TestCapturedFiles | Should -Not -BeNullOrEmpty - $script:TestCapturedFiles | ForEach-Object { - $_ | Should -BeLike '*NerdFont*' - $_ | Should -Not -BeLike '*NerdFontMono*' - $_ | Should -Not -BeLike '*NerdFontPropo*' - } - } finally { - InModuleScope NerdFonts -Parameters @{ fonts = $originalFonts } { - param($fonts) - $script:NerdFonts = $fonts - } - } - } - - It 'Install-NerdFont - Installs a font with -Variant Propo' { - $originalFonts = InModuleScope NerdFonts { $script:NerdFonts } - $loadedFonts = Get-Content -Path (Join-Path -Path $PSScriptRoot -ChildPath '../src/FontsData.json') | ConvertFrom-Json - $goodFont = $loadedFonts | Where-Object Name -EQ 'Hack' | Select-Object -First 1 - $testFonts = @($goodFont) - InModuleScope NerdFonts -Parameters @{ fonts = $testFonts } { - param($fonts) - $script:NerdFonts = $fonts - } - - try { - Mock -ModuleName NerdFonts Get-Font { @() } - $script:TestCapturedFiles = $null - Mock -ModuleName NerdFonts Install-Font {} -ParameterFilter { - $script:TestCapturedFiles = @( - Get-ChildItem -Path $Path -Recurse -File -Include '*.ttf', '*.otf' | - Select-Object -ExpandProperty Name - ) - $true - } + Install-NerdFont -Name 'Hack' -Variant $Variant -ErrorAction Stop - { Install-NerdFont -Name 'Hack' -Variant Propo -ErrorAction Stop } | Should -Not -Throw - Should -Invoke -ModuleName NerdFonts Install-Font -Times 1 -Exactly - $script:TestCapturedFiles | Should -Not -BeNullOrEmpty - $script:TestCapturedFiles | ForEach-Object { $_ | Should -BeLike '*NerdFontPropo*' } - } finally { - InModuleScope NerdFonts -Parameters @{ fonts = $originalFonts } { - param($fonts) - $script:NerdFonts = $fonts + Should-Invoke -CommandName Install-Font -ModuleName NerdFonts -Times 1 -Exactly + $script:TestCapturedFiles | Should-NotBeNull + $script:TestCapturedFiles | Should-All { $_ -like $Expected } + foreach ($pattern in $NotExpected) { + $script:TestCapturedFiles | Should-All { $_ -notlike $pattern } } } } It 'Install-NerdFont - Handles -All without downloading already installed fonts' { - $originalFonts = InModuleScope NerdFonts { $script:NerdFonts } $testFonts = @( [pscustomobject]@{ Name = 'AllPathSmokeTest' URL = 'https://example.invalid/all-path-smoke.zip' } ) - InModuleScope NerdFonts -Parameters @{ fonts = $testFonts } { - param($fonts) - $script:NerdFonts = $fonts - } - try { + Use-TestFontData -Fonts $testFonts -Body { Mock -ModuleName NerdFonts Get-Font { [pscustomobject]@{ Name = 'AllPathSmokeTest Nerd Font' } } Mock -ModuleName NerdFonts Install-Font {} - { Install-NerdFont -All -Verbose -ErrorAction Stop } | Should -Not -Throw - Should -Invoke -ModuleName NerdFonts Install-Font -Times 0 -Exactly - } finally { - InModuleScope NerdFonts -Parameters @{ fonts = $originalFonts } { - param($fonts) - $script:NerdFonts = $fonts - } + Install-NerdFont -All -Verbose -ErrorAction Stop + + Should-NotInvoke -CommandName Install-Font -ModuleName NerdFonts } } It 'Install-NerdFont - Throws when -Scope AllUsers without admin rights' { Mock -ModuleName NerdFonts IsAdmin { $false } - { Install-NerdFont -Name 'Tinos' -Scope AllUsers -ErrorAction Stop } | Should -Throw '*Administrator*' + { Install-NerdFont -Name 'Tinos' -Scope AllUsers -ErrorAction Stop } | + Should-Throw -ExceptionMessage '*Administrator*' } It 'Install-NerdFont - Falls back to download when cache read fails' { - $originalFonts = InModuleScope NerdFonts { $script:NerdFonts } - $loadedFonts = Get-Content -Path (Join-Path -Path $PSScriptRoot -ChildPath '../src/FontsData.json') | ConvertFrom-Json - $goodFont = $loadedFonts | Where-Object Name -EQ 'Tinos' | Select-Object -First 1 + $goodFont = Get-TestFont -Name 'Tinos' $fontName = $goodFont.Name - $cacheRoot = if ($IsWindows) { - Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'PSModule/NerdFonts/cache' - } else { - Join-Path $HOME '.cache/PSModule/NerdFonts' - } + $cacheRoot = Get-TestCacheRoot $cacheTag = if ($goodFont.URL -match '/releases/download/([^/]+)/') { $Matches[1] } else { 'unknown' } - $cacheTagDir = Join-Path $cacheRoot $cacheTag + $cacheTagDir = Join-Path -Path $cacheRoot -ChildPath $cacheTag $downloadFileName = Split-Path -Path $goodFont.URL -Leaf - $cachedFile = Join-Path $cacheTagDir $downloadFileName + $cachedFile = Join-Path -Path $cacheTagDir -ChildPath $downloadFileName # Backup any existing real cache entry to restore after the test $backupPath = "$cachedFile.test-bak" @@ -259,36 +236,33 @@ Describe 'Module' { Copy-Item -LiteralPath $cachedFile -Destination $backupPath -Force } - $testFonts = @($goodFont) - InModuleScope NerdFonts -Parameters @{ fonts = $testFonts } { - param($fonts) - $script:NerdFonts = $fonts - } - $fileLock = $null try { - # Lock the cached file with an exclusive share so Copy-Item fails, forcing the - # function to fall back to a real download using live test data. - if (-not (Test-Path -LiteralPath $cacheTagDir)) { - $null = New-Item -ItemType Directory -Path $cacheTagDir -Force - } - if (Test-Path -LiteralPath $cachedFile) { - Remove-Item -LiteralPath $cachedFile -Recurse -Force -ErrorAction SilentlyContinue - } - Set-Content -LiteralPath $cachedFile -Value 'locked-cache-entry' -Force - $fileLock = [System.IO.File]::Open( - $cachedFile, - [System.IO.FileMode]::Open, - [System.IO.FileAccess]::Read, - [System.IO.FileShare]::None - ) + Use-TestFontData -Fonts @($goodFont) -Body { + # Lock the cached file with an exclusive share so Copy-Item fails, forcing the + # function to fall back to a real download using live test data. + if (-not (Test-Path -LiteralPath $cacheTagDir)) { + $null = New-Item -ItemType Directory -Path $cacheTagDir -Force + } + if (Test-Path -LiteralPath $cachedFile) { + Remove-Item -LiteralPath $cachedFile -Recurse -Force -ErrorAction SilentlyContinue + } + Set-Content -LiteralPath $cachedFile -Value 'locked-cache-entry' -Force + $fileLock = [System.IO.File]::Open( + $cachedFile, + [System.IO.FileMode]::Open, + [System.IO.FileAccess]::Read, + [System.IO.FileShare]::None + ) - Mock -ModuleName NerdFonts Get-Font { @() } - Mock -ModuleName NerdFonts Install-Font {} + Mock -ModuleName NerdFonts Get-Font { @() } + Mock -ModuleName NerdFonts Install-Font {} - # Should not throw — falls back to download - { Install-NerdFont -Name $fontName -Force:$false -ErrorAction Stop } | Should -Not -Throw - Should -Invoke -ModuleName NerdFonts Install-Font -Times 1 -Exactly + # Falls back to a real download instead of failing on the unreadable cache entry. + Install-NerdFont -Name $fontName -Force:$false -ErrorAction Stop + + Should-Invoke -CommandName Install-Font -ModuleName NerdFonts -Times 1 -Exactly + } } finally { # Release the lock before restoring cache state. if ($fileLock) { @@ -308,21 +282,12 @@ Describe 'Module' { if (-not $hadExistingCacheRoot -and (Test-Path -LiteralPath $cacheRoot)) { Remove-Item -LiteralPath $cacheRoot -Recurse -Force -ErrorAction SilentlyContinue } - InModuleScope NerdFonts -Parameters @{ fonts = $originalFonts } { - param($fonts) - $script:NerdFonts = $fonts - } } } It 'Install-NerdFont - Deduplicates variant files from cached archives' { - $originalFonts = InModuleScope NerdFonts { $script:NerdFonts } $fontName = 'DuplicateMonoTest' - $cacheRoot = if ($IsWindows) { - Join-Path -Path ([Environment]::GetFolderPath('LocalApplicationData')) -ChildPath 'PSModule/NerdFonts/cache' - } else { - Join-Path -Path $HOME -ChildPath '.cache/PSModule/NerdFonts' - } + $cacheRoot = Get-TestCacheRoot $cacheTagDir = Join-Path -Path $cacheRoot -ChildPath 'test-dedup-v0' $zipPath = Join-Path -Path $cacheTagDir -ChildPath 'DuplicateMonoTest.zip' $hadExistingCacheRoot = Test-Path -LiteralPath $cacheRoot @@ -354,25 +319,24 @@ Describe 'Module' { URL = 'https://github.com/ryanoasis/nerd-fonts/releases/download/test-dedup-v0/DuplicateMonoTest.zip' } ) - InModuleScope NerdFonts -Parameters @{ fonts = $testFonts } { - param($fonts) - $script:NerdFonts = $fonts - } - Mock -ModuleName NerdFonts Get-Font { @() } - $script:TestCapturedFiles = $null - Mock -ModuleName NerdFonts Install-Font {} -ParameterFilter { - $script:TestCapturedFiles = @( - Get-ChildItem -Path $Path -Recurse -File -Include '*.ttf', '*.otf' | - Select-Object -ExpandProperty Name - ) - $true - } + Use-TestFontData -Fonts $testFonts -Body { + Mock -ModuleName NerdFonts Get-Font { @() } + $script:TestCapturedFiles = $null + Mock -ModuleName NerdFonts Install-Font {} -ParameterFilter { + $script:TestCapturedFiles = @( + Get-ChildItem -Path $Path -Recurse -File -Include '*.ttf', '*.otf' | + Select-Object -ExpandProperty Name + ) + $true + } - { Install-NerdFont -Name $fontName -Variant Mono -ErrorAction Stop } | Should -Not -Throw - Should -Invoke -ModuleName NerdFonts Install-Font -Times 1 -Exactly - $script:TestCapturedFiles.Count | Should -Be 1 - ($script:TestCapturedFiles | Select-Object -Unique).Count | Should -Be 1 + Install-NerdFont -Name $fontName -Variant Mono -ErrorAction Stop + + Should-Invoke -CommandName Install-Font -ModuleName NerdFonts -Times 1 -Exactly + $script:TestCapturedFiles | Should-BeCollection -Count 1 + ($script:TestCapturedFiles | Select-Object -Unique).Count | Should-Be 1 + } } finally { if (Test-Path -LiteralPath $cacheTagDir) { Remove-Item -LiteralPath $cacheTagDir -Recurse -Force -ErrorAction SilentlyContinue @@ -380,10 +344,6 @@ Describe 'Module' { if (-not $hadExistingCacheRoot -and (Test-Path -LiteralPath $cacheRoot)) { Remove-Item -LiteralPath $cacheRoot -Recurse -Force -ErrorAction SilentlyContinue } - InModuleScope NerdFonts -Parameters @{ fonts = $originalFonts } { - param($fonts) - $script:NerdFonts = $fonts - } } } }