tests/DFCatalogDetail.Tests.ps1
|
BeforeAll { . "$PSScriptRoot/../Public/New-DFDirectory.ps1" . "$PSScriptRoot/../Private/DFCatalog.ps1" . "$PSScriptRoot/../Private/Start-DFCatalogRefreshJob.ps1" # must exist for Mock } Describe 'New-DFToolSourceDetail' { It 'builds a typed object with all fields' { $d = New-DFToolSourceDetail -Source npm -PackageId left-pad ` -Publisher 'npm, Inc.' -Maintainers @('alice') -Dependencies @('lodash@^4') ` -Tags @('cli') -Downloads 12345 -RepositoryUrl 'https://github.com/x/y' ` -InstallHint 'npm install -g left-pad' -Readme '# hi' $d.PSObject.TypeNames[0] | Should -Be 'DotForge.ToolSourceDetail' $d.Source | Should -Be 'npm' $d.Downloads | Should -Be 12345 $d.Maintainers | Should -Be @('alice') $d.Readme | Should -Be '# hi' } It 'defaults optional fields to empty/null' { $d = New-DFToolSourceDetail -Source scoop -PackageId 'main/rg' $d.Downloads | Should -BeNullOrEmpty @($d.Tags).Count | Should -Be 0 } } Describe 'Get-DFCatalogDetailCache' { BeforeEach { $script:SavedXdgCache = $Env:XDG_CACHE_HOME $Env:XDG_CACHE_HOME = Join-Path $TestDrive 'cache' # Pester 5 does not clear TestDrive between Its; wipe the cache dir so # each test starts from a genuine miss. if (Test-Path $Env:XDG_CACHE_HOME) { Remove-Item $Env:XDG_CACHE_HOME -Recurse -Force } } AfterEach { $Env:XDG_CACHE_HOME = $script:SavedXdgCache } It 'fetches live on miss and writes the cache file' { $d = Get-DFCatalogDetailCache -Provider npm -PackageId left-pad -Fetch { param($id) New-DFToolSourceDetail -Source npm -PackageId $id -Publisher 'p' } $d.Publisher | Should -Be 'p' $key = (ConvertTo-DFCatalogQueryKey -Query 'left-pad').Key Test-Path (Join-Path $Env:XDG_CACHE_HOME "dotforge/catalogs/npm/details/$key.json") | Should -BeTrue } It 'serves a fresh cache hit without invoking Fetch' { $null = Get-DFCatalogDetailCache -Provider npm -PackageId left-pad -Fetch { param($id) New-DFToolSourceDetail -Source npm -PackageId $id -Publisher 'cached' } $d = Get-DFCatalogDetailCache -Provider npm -PackageId left-pad -Fetch { throw 'must not fetch' } $d.Publisher | Should -Be 'cached' } It 'stores the RAW PackageId in the envelope query field (for re-warm)' { $null = Get-DFCatalogDetailCache -Provider scoop -PackageId 'Extras/Zed' -Fetch { param($id) New-DFToolSourceDetail -Source scoop -PackageId $id } $key = (ConvertTo-DFCatalogQueryKey -Query 'Extras/Zed').Key $file = Join-Path $Env:XDG_CACHE_HOME "dotforge/catalogs/scoop/details/$key.json" (Get-Content $file -Raw | ConvertFrom-Json).query | Should -BeExactly 'Extras/Zed' } It 'kicks a background detail re-warm on a stale hit (registered provider)' { Mock Start-DFCatalogRefreshJob { } # Background re-warm only fires for a REGISTERED provider — register a # stand-in npm entry so this test reflects that precondition. $script:DFCatalogProviders['npm'] = @{ Name = 'npm' } try { $null = Get-DFCatalogDetailCache -Provider npm -PackageId left-pad -Fetch { param($id) New-DFToolSourceDetail -Source npm -PackageId $id } $script:DFCatalogTtl['npm'] = [timespan]::FromMinutes(-1) # force stale try { $d = Get-DFCatalogDetailCache -Provider npm -PackageId left-pad -Fetch { throw 'no inline fetch' } $d | Should -Not -BeNullOrEmpty Should -Invoke Start-DFCatalogRefreshJob -ParameterFilter { $Kind -eq 'detail' -and $Query -eq 'left-pad' } } finally { $script:DFCatalogTtl.Remove('npm') } } finally { $script:DFCatalogProviders.Remove('npm') } } It 'treats a stale hit as a miss for an unregistered provider (no background job, inline refresh)' { Mock Start-DFCatalogRefreshJob { } $null = Get-DFCatalogDetailCache -Provider github -PackageId 'owner/repo' -Fetch { param($id) New-DFToolSourceDetail -Source github -PackageId $id -Publisher 'old' } $script:DFCatalogTtl['github'] = [timespan]::FromMinutes(-1) # force stale try { $d = Get-DFCatalogDetailCache -Provider github -PackageId 'owner/repo' -Fetch { param($id) New-DFToolSourceDetail -Source github -PackageId $id -Publisher 'fresh' } $d.Publisher | Should -Be 'fresh' Should -Invoke Start-DFCatalogRefreshJob -Times 0 } finally { $script:DFCatalogTtl.Remove('github') } } It 'falls back to the stale cache for an unregistered provider when the inline refresh throws' { Mock Start-DFCatalogRefreshJob { } $null = Get-DFCatalogDetailCache -Provider github -PackageId 'owner/repo' -Fetch { param($id) New-DFToolSourceDetail -Source github -PackageId $id -Publisher 'old' } $script:DFCatalogTtl['github'] = [timespan]::FromMinutes(-1) # force stale try { $d = Get-DFCatalogDetailCache -Provider github -PackageId 'owner/repo' -Fetch { throw 'api down' } $d.Publisher | Should -Be 'old' Should -Invoke Start-DFCatalogRefreshJob -Times 0 } finally { $script:DFCatalogTtl.Remove('github') } } It 'falls back to stale cache when the live fetch throws' { $null = Get-DFCatalogDetailCache -Provider npm -PackageId left-pad -Fetch { param($id) New-DFToolSourceDetail -Source npm -PackageId $id -Publisher 'old' } $d = Get-DFCatalogDetailCache -Provider npm -PackageId left-pad -Fresh -Fetch { throw 'api down' } $d.Publisher | Should -Be 'old' } It 'returns null (no throw, no cache write) when fetch returns nothing' { $d = Get-DFCatalogDetailCache -Provider npm -PackageId nope -Fetch { $null } $d | Should -BeNullOrEmpty } } Describe 'Get-DFCatalogDetail dispatcher' { BeforeEach { $script:SavedProviders = $script:DFCatalogProviders $script:DFCatalogProviders = @{ npm = @{ Name = 'npm'; Detail = { param($Id, $Fresh) New-DFToolSourceDetail -Source npm -PackageId $Id -Publisher "fresh=$Fresh" } } bare = @{ Name = 'bare' } # no Detail hook } } AfterEach { $script:DFCatalogProviders = $script:SavedProviders } It 'invokes the provider Detail hook with id and fresh flag' { (Get-DFCatalogDetail -Source npm -PackageId x -Fresh).Publisher | Should -Be 'fresh=True' } It 'returns null for providers without a Detail hook' { Get-DFCatalogDetail -Source bare -PackageId x | Should -BeNullOrEmpty } It 'swallows a throwing hook and returns null' { $script:DFCatalogProviders.npm.Detail = { throw 'boom' } Get-DFCatalogDetail -Source npm -PackageId x | Should -BeNullOrEmpty } } Describe 'New-DFToolInfo Details/GitHub properties' { It 'always carries Details and GitHub (null by default)' { $i = New-DFToolInfo -Name x -Sources @() $i.PSObject.Properties.Name | Should -Contain 'Details' $i.PSObject.Properties.Name | Should -Contain 'GitHub' $i.Details | Should -BeNullOrEmpty } } |