From 852b4f1d95389ecefbfc8a621baf1cd0e3d63c5e Mon Sep 17 00:00:00 2001 From: "Rodweil, Theodor" Date: Sun, 6 Aug 2023 18:47:16 +0200 Subject: [PATCH] refactor(PageMeta): move page metadata stuff to seperate module test(PageMeta): fix scoping of page metadata module tests fix(PageMeta): update for support of new manifest format --- PSConfluencePublisher/PageMeta.Tests.ps1 | 256 +++++++++++++++++++++++ PSConfluencePublisher/PageMeta.psm1 | 199 ++++++++++++++++++ 2 files changed, 455 insertions(+) create mode 100755 PSConfluencePublisher/PageMeta.Tests.ps1 create mode 100755 PSConfluencePublisher/PageMeta.psm1 diff --git a/PSConfluencePublisher/PageMeta.Tests.ps1 b/PSConfluencePublisher/PageMeta.Tests.ps1 new file mode 100755 index 0000000..3d9eebe --- /dev/null +++ b/PSConfluencePublisher/PageMeta.Tests.ps1 @@ -0,0 +1,256 @@ +#!/usr/bin/env pwsh +$ErrorActionPreference = "Stop" + +BeforeAll { + Import-Module (Join-Path $PSScriptRoot 'PSConfluencePublisher.psd1') -Force +} + + +Describe 'Get-PageMetaCache' ` +{ + Context 'default' ` + { + It 'uses index' ` + { + $mockPageMeta = @{ + 'Title' = 'foobar' + } + + $mockManifest = @( + $mockPageMeta + ) + + $mockIndex = @{ + 'foobar' = 0 + } + + $meta = Get-PageMetaCache ` + -Title 'foobar' ` + -Manifest $mockManifest ` + -Index $mockIndex + + $meta | Should -Be $mockPageMeta + } + + It 'returns page meta when title exists' ` + { + $mockPageMeta = @{ + 'Title' = 'foobar' + } + + $mockManifest = @( + $mockPageMeta + ) + + $meta = Get-PageMetaCache ` + -Title 'foobar' ` + -Manifest $mockManifest + + $meta | Should -Be $mockPageMeta + } + + It 'returns null, if page with supplied title does not exist' ` + { + $mockManifest = @( + @{} + ) + + $meta = Get-PageMetaCache ` + -Title 'foobar' ` + -Manifest $mockManifest + + $meta | Should -Be $null + } + } +} + + +Describe 'Get-PageMeta' ` +{ + Context 'default' ` + { + BeforeAll ` + { + Mock -ModuleName 'PageMeta' Get-PersonalAccessToken { + '012345678901234567890' + } + } + + It 'returns cache when page id present' ` + { + $mockPageMeta = @{ + 'Title' = 'foobar' + 'PageId' = '0123456789' + } + + $mockManifest = @( + $mockPageMeta + ) + + Mock -ModuleName 'PageMeta' Get-PageMetaCache { + $mockPageMeta + } + + $meta = Get-PageMeta ` + -Host 'foobar' ` + -Title 'foobar' ` + -Space 'foobar' ` + -Manifest $mockManifest + + $meta | Should -Be $mockPageMeta + + Should -Invoke -CommandName 'Get-PageMetaCache' ` + -ModuleName 'PageMeta' ` + -Exact ` + -Times 1 + } + + It 'gets a page id remotely if there is exactly one result' ` + { + $mockPageMeta = @{ + 'Version' = 'version' + 'Hash' = 'hash' + 'Ref' = 'ref' + } + + Mock -ModuleName 'PageMeta' Get-PageMetaCache { + $mockPageMeta + } + + Mock -ModuleName 'PageMeta' Update-PageMeta { + $PageId | Should -Be '123' + + $Version | Should -Be 9 + + $Title | Should -Be 'foobar' + + $mockPageMeta + } + + Mock -ModuleName 'PageMeta' Invoke-WebRequest { + @{ + 'Content' = '{"results": [{"id": "123","_expandable":{"version": 9}}]}' + } + } + + $meta = Get-PageMeta ` + -Host 'confluence.contoso.com' ` + -Title 'foobar' ` + -Space 'foobar' ` + -Manifest @{'Pages'= {}} + + $meta | Should -Be $mockPageMeta + + Should -Invoke 'Get-PageMetaCache' ` + -ModuleName 'PageMeta' ` + -Exactly ` + -Times 1 + + Should -Invoke 'Invoke-WebRequest' ` + -ModuleName 'PageMeta' ` + -Exactly ` + -Times 1 + + Should -Invoke 'Update-PageMeta' ` + -ModuleName 'PageMeta' ` + -Exactly ` + -Times 1 + } + + It 'throws an exception, if there is more than one result' ` + { + Mock -ModuleName 'PageMeta' Invoke-WebRequest { + @{ + 'Content' = '{"results": [{}, {}]}' + } + } + + { + Get-PageMeta ` + -Host 'confluence.contoso.com' ` + -Title 'foobar' ` + -Space 'foobar' ` + -Manifest @{'Pages'= {}} + } | Should -Throw + } + + It 'throws an exception, if there is no result' ` + { + Mock Invoke-WebRequest { + @{ + 'Content' = '{"results": []}' + } + } + + { + Get-PageMeta ` + -Host 'confluence.contoso.com' ` + -Title 'foobar' ` + -Space 'foobar' ` + -Manifest @{'Pages'= {}} + } | Should -Throw + } + } +} + + +Describe 'Update-PageMeta' ` +{ + Context 'default' ` + { + It 'fails, if page meta index does not exist' ` + { + { + Update-PageMeta ` + -PageId '0123456789' ` + -Title 'foobar' ` + -Manifest @{} + } | Should -Throw + } + + It 'updates minimal' ` + { + $mockPageMeta = @{ + 'Title' = 'foobar' + } + + $mockManifest = @( + $mockPageMeta + ) + + $pageMeta = Update-PageMeta ` + -Title 'foobar' ` + -PageId '0123456789' ` + -Manifest $mockManifest + + $mockPageMeta.PageId | Should -Be '0123456789' + } + + It 'updates extended' ` + { + $mockPageMeta = @{ + 'Title' = 'foobar' + } + + $mockManifest = @( + $mockPageMeta + ) + + Update-PageMeta ` + -Title 'foobar' ` + -PageId 'pageId' ` + -Version 9001 ` + -AncestorTitle 'ancestorTitle' ` + -Hash 'hash' ` + -Manifest $mockManifest + + $mockPageMeta.PageId | Should -Be 'pageId' + + $mockPageMeta.Version | Should -Be 9001 + + $mockPageMeta.AncestorTitle | Should -Be 'ancestorTitle' + + $mockPageMeta.Hash | Should -Be 'hash' + } + } +} diff --git a/PSConfluencePublisher/PageMeta.psm1 b/PSConfluencePublisher/PageMeta.psm1 new file mode 100755 index 0000000..dbf49ae --- /dev/null +++ b/PSConfluencePublisher/PageMeta.psm1 @@ -0,0 +1,199 @@ +#!/usr/bin/env pwsh +$ErrorActionPreference = "Stop" + + +function Get-PageMetaCache +{ + <# + .SYNOPSIS + Get a locally indexed/cached Confluence page id + + .EXAMPLE + Get-PageMetaCache ` + -Title 'Page Title' ` + -Manifest @() ` + -Index @{} + + .NOTES + To test or not to test, that is the question... Since the + `Test-JSON` cmdlet requires serialized JSON, but we are working with + the deserialized Hashtable, it's too computationally intense to + always test the input upon every call. We therefore only make sure, + that correct data is written to the filesystem. For the rest, each + function is responsible for themself (learned that that's a valid + reflexive pronoun today 🤓). + + This function is lucky to get this note, because it's at the top 💯. + Of course this applies to every function. + #> + Param( + [Parameter(Mandatory)] [string] $Title, + [Parameter(Mandatory)] [Array] $Manifest, + [Parameter()] [Collections.Hashtable] $Index + ) + + Process + { + If ($Index -And $Manifest.Count -gt 0 -And $Manifest[$Index.$Title]) + { + $Manifest[$Index.$Title] + } + + Else + { + For ($i = 0; $i -lt $Manifest.Count; $i += 1) + { + If ($Manifest[$i].Title -eq $Title) + { + $Manifest[$i] + + break + } + } + } + } +} + + +function Get-PageMeta +{ + <# + .SYNOPSIS + Get a Confluence page id + + .DESCRIPTION + First, tries to retrieve from local page id index (cache) through + the local alias. If no cache hit, then polls the Confluence + instance host for the id by providing a space key and page title. + + .EXAMPLE + Get-PageMeta ` + -Host 'confluence.contoso.com' ` + -Title 'Testitest' ` + -Space 'TIARA' ` + -CacheIndexFile 'confluence-page-cache.json' + #> + Param( + [Parameter(Mandatory)] [string] $Host, + [Parameter(Mandatory)] [string] $Title, + [Parameter(Mandatory)] [string] $Space, + [Parameter(Mandatory)] [Array] $Manifest, + [Parameter()] [Collections.Hashtable] $Index + ) + + Begin + { + $pageMeta = Get-PageMetaCache ` + -Title $Title ` + -Manifest $Manifest ` + -Index $Index + } + + Process + { + If ($pageMeta -And $pageMeta.PageId) + { + $pageMeta + + return + } + + $escapedTitle = [Uri]::EscapeDataString($Title) + + #TODO: move this to a separate function + $query = "title=${escapedTitle}&spaceKey=${Space}&expand=history" + + Invoke-WebRequest ` + -Uri "https://${Host}/rest/api/content?$query" ` + -Method 'Get' ` + -Headers @{ + 'Authorization' = "Bearer $(Get-PersonalAccessToken $Host)" + } ` + -OutVariable response | Out-Null + + $results = ($response.Content | ConvertFrom-JSON).results + + if ($results.Count -gt 1) + { + throw "more than one result for query: $query" + } + + elseif ($results.Count -eq 1) + { + Update-PageMeta ` + -PageId $results[0].id ` + -Version ($results[0]._expandable | Select -ExpandProperty 'version') ` + -Title $Title ` + -Manifest $Manifest + } + } +} + + +function Update-PageMeta +{ + <# + .SYNOPSIS + Register a Confluence page's metadata in the local cache + + .DESCRIPTION + Synchronizes the locally cached page metadata (in manifest) with the + data stored by the Confluence instance. Therefore it is required to + supply a page id, since this is the reference linking the locally + cached page to a published instance of a page. + + .EXAMPLE + Update-PageMeta ` + -Title 'foobar' ` + -PageId 'pageId' ` + -Version 9001 ` + -AncestorTitle 'ancestorTitle' ` + -Hash 'hash' ` + -Manifest $mockManifest + #> + Param( + [Parameter(Mandatory)] [String] $Title, + [Parameter(Mandatory)] [String] $PageId, + [Parameter()] [Int] $Version, + [Parameter()] [String] $AncestorTitle, + [Parameter()] [String] $Hash, + [Parameter(Mandatory)] [Array] $Manifest, + [Parameter()] [Collections.Hashtable] $Index + ) + + Process + { + $pageMeta = Get-PageMetaCache ` + -Title $Title ` + -Manifest $Manifest ` + -Index $Index + + If (-Not $pageMeta) + { + throw "page titled `$Title` not indexed in Manifest." + } + + $pageMeta.PageId = $PageId + + If ($Version) + { + $pageMeta.Version = $Version + } + + If ($AncestorTitle) + { + $pageMeta.AncestorTitle = $AncestorTitle + } + + # if content didn't update, hash stays the same + If ($Hash) + { + $pageMeta.Hash = $Hash + } + + Write-Debug "register: $Title -> $PageId" + + $pageMeta + } +} +