diff --git a/PSConfluencePublisher/Page.Tests.ps1 b/PSConfluencePublisher/Page.Tests.ps1 index ec2f8b2..1c83d2b 100755 --- a/PSConfluencePublisher/Page.Tests.ps1 +++ b/PSConfluencePublisher/Page.Tests.ps1 @@ -1,287 +1,97 @@ #!/usr/bin/env pwsh $ErrorActionPreference = "Stop" + BeforeAll { Import-Module (Join-Path $PSScriptRoot 'PSConfluencePublisher.psd1') -Force } -AfterAll { - -} - - -Describe 'Get-CachedPageMeta' ` -{ - Context 'Parameterized' ` - { - It 'returns page meta when title exists' ` - { - $mockPageName = 'Testitest' - - $mockPageMeta = @{ - 'foo' = 'bar' - } - - $mockManifest = @{ - 'pages' = @{ - $mockPageName = $mockPageMeta - } - } - - $meta = Get-CachedPageMeta ` - -Title $mockPageName ` - -Manifest $mockManifest - - $meta | Should -Be $mockPageMeta - } - - It 'returns null of title does not exist' ` - { - $mockPageName = 'Testitest' - - - $mockManifest = @{ - 'pages' = @{} - } - - $meta = Get-CachedPageMeta ` - -Title $mockPageName ` - -Manifest $mockManifest - - $meta | Should -Be $null - } - } -} - - -Describe 'Get-PageMeta' ` -{ - Context 'Parameterized' ` - { - It 'returns cache when present' ` - { - InModuleScope Page ` - { - $mockPageMeta = @{ - 'foo' = 'bar' - } - - Mock Get-CachedPageMeta { - $mockPageMeta - } - - $meta = Get-PageMeta ` - -Host 'foobar' ` - -Title 'foobar' ` - -Space 'foobar' ` - -Manifest @{} - - $meta | Should -Be $mockPageMeta - } - } - - It 'gets a page id remotely if there is exactly one result' ` - { - InModuleScope Page ` - { - $mockPageMeta = @{ - 'PageId' = 'page_id' - 'Version' = 'version' - 'Hash' = 'hash' - 'Ref' = 'ref' - } - - Mock Get-PersonalAccessToken {"012345678901234567890"} - - Mock Update-PageMeta { - #FIXME: wrong scope - Should -Invoke 'Update-PageMeta' -Exactly -Times 1 - - $PageId | Should -Be '123' - - $Version | Should -Be 9 - - $Title | Should -Be 'foobar' - - $mockPageMeta - } - - Mock 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 - } - } - - It 'throws an exception, if there is more than one result' ` - { - InModuleScope Page ` - { - Mock Get-PersonalAccessToken {"012345678901234567890"} - - Mock 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' ` - { - InModuleScope Page ` - { - Mock Get-PersonalAccessToken {"012345678901234567890"} - - Mock Invoke-WebRequest { - @{ - 'Content' = '{"results": [{}, {}]}' - } - } - - { - Get-PageMeta ` - -Host 'confluence.contoso.com' ` - -Title 'foobar' ` - -Space 'foobar' ` - -Manifest @{'Pages'= {}} - } | Should -Throw - } - } - } -} - - -Describe 'Update-PageMeta' ` -{ - Context 'Parameterized' ` - { - It 'fails, if page meta index does not exist' ` - { - { - Update-PageMeta ` - -PageId 'foobar' ` - -Title 'foobar' ` - -Manifest @{} - } | Should -Throw - } - - It 'fails, if page title is not indexed' ` - { - { - Update-PageMeta ` - -PageId 'foobar' ` - -Title 'foobar' ` - -Manifest @{'Pages' = @{}} - } | Should -Throw - } - - It 'updates minimal' ` - { - $mockManifest = @{'Pages' = @{'foobar' = @{}}} - - Update-PageMeta ` - -Title 'foobar' ` - -PageId 'pageId' ` - -Manifest $mockManifest - - $mockManifest.Pages.foobar.PageId | Should -Be 'pageId' - } - - It 'updates extended' ` - { - $mockManifest = @{'Pages' = @{'foobar' = @{}}} - - Update-PageMeta ` - -Title 'foobar' ` - -PageId 'pageId' ` - -Version 9001 ` - -AncestorTitle 'ancestorTitle' ` - -Hash 'hash' ` - -Manifest $mockManifest - - $mockManifest.Pages.foobar.PageId | Should -Be 'pageId' - - $mockManifest.Pages.foobar.Version | Should -Be 9001 - - $mockManifest.Pages.foobar.AncestorTitle | Should -Be 'ancestorTitle' - - $mockManifest.Pages.foobar.Hash | Should -Be 'hash' - } - } -} - - Describe 'New-Page' ` { - Context 'Parameterized' ` + Context 'default' ` { - It 'fails, if page meta index does not exist' ` + BeforeAll ` { - InModuleScope Page ` - { - $mockManifest = @{ - 'Pages' = @{ - 'title' = @{ - 'Ref' = 'pages/320okffs.xml' - } - } - } - - Mock Get-Content { - $Path | Should -Be 'pages/320okffs.xml' - - 'foobar' - } - - Mock Get-PersonalAccessToken { - '01234567890123456789' - } - - Mock Invoke-WebRequest { - $Uri | Should -Be 'https://confluence.contoso.com/rest/api/content' - - $body_ = $Body | ConvertFrom-JSON - - $body_.type | Should -Be 'page' - - $body_.body.storage.representation | Should -Be 'storage' - - $body_.body.storage.value | Should -Be 'foobar' - - $body_.space.key | Should -Be 'testitest' - - $body_.title | Should -Be 'title' - - @{ - 'Content' = '{"Id": "123", "version": {"number": "1"}}' - } - } - - New-Page ` - -Host 'confluence.contoso.com' ` - -Space 'testitest' ` - -Title 'title' ` - -Manifest $mockManifest + Mock -ModuleName 'Page' Get-Content { + 'foobar content' } + + Mock -ModuleName 'Page' Get-PersonalAccessToken { + '01234567890123456789' + } + } + + It 'succeeds' ` + { + $mockPageMeta = @{ + 'Title' = 'foobar' + 'Ref' = 'pages/320okffs.xml' + } + + $mockManifest = @( + $mockPageMeta + ) + + Mock -ModuleName 'Page' Get-PageMeta { + $mockPageMeta + } + + Mock -ModuleName 'Page' Update-PageMeta { + $Id | Should -Be '123' + + $mockPageMeta.Id = '123' + + $mockPageMeta.Version = 1 + + $mockPageMeta.Hash = 'NOTAREALHASH' + + $mockPageMeta + } + + Mock -ModuleName 'Page' Invoke-WebRequest { + $Uri | Should -Be 'https://confluence.contoso.com/rest/api/content' + + $body_ = $Body | ConvertFrom-JSON + + $body_.type | Should -Be 'page' + + $body_.body.storage.representation | Should -Be 'storage' + + $body_.body.storage.value | Should -Be 'foobar content' + + $body_.space.key | Should -Be 'testitest' + + $body_.title | Should -Be 'title' + + @{ + 'Content' = '{"Id": "123", "version": {"number": 1}}' + } + } + + New-Page ` + -Host 'confluence.contoso.com' ` + -Space 'testitest' ` + -Title 'title' ` + -Manifest $mockManifest + + $mockPageMeta.Id | Should -Be "123" + + $mockPageMeta.Version | Should -Be 1 + + $mockPageMeta.Hash | Should -Be ( + 'NOTAREALHASH' + ) + + Should -Invoke -CommandName 'Get-PageMeta' ` + -ModuleName 'Page' ` + -Exactly ` + -Times 1 + + Should -Invoke -CommandName 'Update-PageMeta' ` + -ModuleName 'Page' ` + -Exactly ` + -Times 1 } } } @@ -289,155 +99,161 @@ Describe 'New-Page' ` Describe 'Update-Page' ` { - Context 'Parameterized' ` + BeforeAll ` { + Mock -ModuleName 'Page' Get-Content { + 'foobar content' + } + + Mock -ModuleName 'Page' Get-PersonalAccessToken { + '01234567890123456789' + } + } + + Context 'default' ` + { + BeforeAll ` + { + Mock -ModuleName 'Page' Get-StringHash { + @{ + 'Hash' = 'NOTAREALHASH' + } + } + } + It 'succeeds' ` { - InModuleScope Page ` - { - $mockManifest = @{ - 'Pages' = @{ - 'mockTitle' = @{ - 'Ref' = 'pages/320okffs.xml' - 'Id' = '0123456789' - } - } - } + $mockPageId = '0123456789' - Mock Get-Content { - $Path | Should -Be 'pages/320okffs.xml' - - 'foobar' - } - - Mock Get-FileHash { - $Path | Should -Be 'pages/320okffs.xml' - - $Algorithm | Should -Be 'SHA256' - - @{ - 'Hash' = 'HASH0123456789' - } - } - - Mock Get-PersonalAccessToken { - '01234567890123456789' - } - - Mock Invoke-WebRequest { - $Uri | Should -Be 'https://confluence.contoso.com/rest/api/content/' - - $body_ = $Body | ConvertFrom-JSON - - $body_.type | Should -Be 'page' - - $body_.body.storage.representation | Should -Be 'storage' - - $body_.body.storage.value | Should -Be 'foobar' - - $body_.space.key | Should -Be 'testitest' - - $body_.title | Should -Be 'mockTitle' - - @{ - 'Content' = '{"Id": "123", "version": {"number": 2}}' - } - } - - Update-Page ` - -Host 'confluence.contoso.com' ` - -Space 'testitest' ` - -Title 'mockTitle' ` - -Manifest $mockManifest - - $mockMeta = $mockManifest.Pages.mockTitle - - $mockMeta.Hash | Should -Be 'HASH0123456789' - - $mockMeta.Version | Should -Be 2 + $mockPageMeta = @{ + 'Title' = 'foobar' + 'Ref' = 'pages/320okffs.xml' + 'Id' = $mockPageId + 'Version' = 3 } + + $mockManifest = @( + $mockPageMeta + ) + + Mock -ModuleName 'Page' Get-PageMeta { + $mockPageMeta + } + + Mock -ModuleName 'Page' Invoke-WebRequest { + $Uri | Should -Be ( + 'https://confluence.contoso.com/rest/api/content/' + ` + $mockPageId + ) + + $body_ = $Body | ConvertFrom-JSON + + $body_.type | Should -Be 'page' + + $body_.body.storage.representation | Should -Be 'storage' + + $body_.body.storage.value | Should -Be 'foobar content' + + $body_.space.key | Should -Be 'testitest' + + $body_.title | Should -Be 'foobar' + + $body_.version.number | Should -Be 4 + + @{ + 'Content' = '{"Id": "123", "version": {"number": 4}}' + } + } + + Update-Page ` + -Host 'confluence.contoso.com' ` + -Space 'testitest' ` + -Title 'foobar' ` + -Manifest $mockManifest + + $mockPageMeta.Hash | Should -Be 'NOTAREALHASH' + + $mockPageMeta.Version | Should -Be 4 } It 'skips, if hash unchanged' ` { - InModuleScope Page ` + $mockPageId = '0123456789' + + $mockPageMeta = @{ + 'Title' = 'foobar' + 'Ref' = 'pages/320okffs.xml' + 'Id' = $mockPageId + 'Version' = 3 + 'Hash' = 'NOTAREALHASH' + } + + $mockManifest = @( + $mockPageMeta + ) + + Mock -ModuleName 'Page' Get-PageMeta { + $mockPageMeta + } + + Update-Page ` + -Host 'confluence.contoso.com' ` + -Space 'testitest' ` + -Title 'mockTitle' ` + -Manifest $mockManifest + } + + It 'fails, if page meta has no reference' ` + { + $mockPageId = '0123456789' + + $mockPageMeta = @{ + 'Title' = 'foobar' + 'Id' = $mockPageId + 'Version' = 3 + } + + $mockManifest = @( + $mockPageMeta + ) + + Mock -ModuleName 'Page' Get-PageMeta { + $mockPageMeta + } + { - $mockManifest = @{ - 'Pages' = @{ - 'mockTitle' = @{ - 'Ref' = 'pages/320okffs.xml' - 'Id' = '0123456789' - 'Hash' = 'HASH0123456789' - } - } - } - - Mock Get-Content { - $Path | Should -Be 'pages/320okffs.xml' - - 'foobar' - } - - Mock Get-FileHash { - $Path | Should -Be 'pages/320okffs.xml' - - $Algorithm | Should -Be 'SHA256' - - @{ - 'Hash' = 'HASH0123456789' - } - } - Update-Page ` -Host 'confluence.contoso.com' ` -Space 'testitest' ` -Title 'mockTitle' ` -Manifest $mockManifest - } - } - - It 'fails, if page meta has no reference' ` - { - InModuleScope Page ` - { - $mockManifest = @{ - 'Pages' = @{ - 'mockTitle' = @{ - 'Id' = '0123456789' - } - } - } - - { - Update-Page ` - -Host 'confluence.contoso.com' ` - -Space 'testitest' ` - -Title 'mockTitle' ` - -Manifest $mockManifest - } | Should -Throw - } + } | Should -Throw "no reference to local content for page*" } It 'fails, if page meta has no id' ` { - InModuleScope Page ` - { - $mockManifest = @{ - 'Pages' = @{ - 'mockTitle' = @{ - 'Ref' = 'pages/320okffs.xml' - } - } - } + $mockPageId = '0123456789' - { - Update-Page ` - -Host 'confluence.contoso.com' ` - -Space 'testitest' ` - -Title 'mockTitle' ` - -Manifest $mockManifest - } | Should -Throw + $mockPageMeta = @{ + 'Title' = 'foobar' + 'Ref' = 'foo/bar' } + + $mockManifest = @( + $mockPageMeta + ) + + Mock -ModuleName 'Page' Get-PageMeta { + $mockPageMeta + } + + { + Update-Page ` + -Host 'confluence.contoso.com' ` + -Space 'testitest' ` + -Title 'mockTitle' ` + -Manifest $mockManifest + } | Should -Throw "no id for page*" } } } diff --git a/PSConfluencePublisher/Page.psm1 b/PSConfluencePublisher/Page.psm1 index 1a68acc..545142a 100755 --- a/PSConfluencePublisher/Page.psm1 +++ b/PSConfluencePublisher/Page.psm1 @@ -2,188 +2,6 @@ $ErrorActionPreference = "Stop" -function Get-CachedPageMeta -{ - <# - .SYNOPSIS - Get a locally indexed/cached Confluence page id - - .EXAMPLE - Get-CachedPageMeta ` - -Title 'Page Title' ` - -Manifest @{...} - - .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)] [Collections.Hashtable] $Manifest - ) - - Process - { - #it's fine this fails, if no `Pages` property is provided, since the - #object (according to the schema) would be invalid anyway. - $pages = $Manifest | Select -ExpandProperty 'Pages' - - try - { - $pageMeta = $pages | Select -ExpandProperty $Title - - Write-Debug "page id cache hit: $Title -> $($pageMeta.PageId)" - - $pageMeta - } - - catch - { - Write-Debug "page id cache miss: $Title" - - $null - } - } -} - - -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)] [Collections.Hashtable] $Manifest - ) - - Process - { - if ($Title) - { - $cachedPageMeta = Get-CachedPageMeta ` - -Title $Title ` - -Manifest $Manifest - } - - if ($cachedPageMeta) - { - return $cachedPageMeta - } - - $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 - - .EXAMPLE - Add-ConfluencePage - -Host 'confluence.contoso.com' ` - -Space 'TIARA' ` - -Title 'Testitest' ` - -Content @{} - #> - Param( - [Parameter(Mandatory)] [string] $Title, - [Parameter(Mandatory)] [string] $PageId, - [Parameter()] [int] $Version, - [Parameter()] [string] $AncestorTitle, - [Parameter()] [string] $Hash, - [Parameter(Mandatory)] [Collections.Hashtable] $Manifest - ) - - Process - { - $metaPages = $Manifest.Pages - - if ((-Not $metaPages) -Or (-Not $metaPages.$Title)) - { - throw "page titled `$Title` not indexed in Manifest." - } - - $meta = $metaPages.$Title - - $meta['PageId'] = $PageId - - if ($Version) - { - $meta['Version'] = $Version - } - - if ($AncestorTitle) - { - $meta['AncestorTitle'] = $AncestorTitle - } - - # if content didn't update, hash stays the same - if ($Hash) - { - $meta['Hash'] = $Hash - } - - Write-Debug "register: $Title -> $PageId" - } -} - - function New-Page { <# @@ -206,20 +24,27 @@ function New-Page [Parameter(Mandatory)] [string] $Space, # title of page to be published [Parameter(Mandatory)] [string] $Title, - # manifest - [Parameter(Mandatory)] [Collections.Hashtable] $Manifest + # pages manifest + [Parameter(Mandatory)] [Array] $Manifest, + # pages manifest index + [Parameter()] [Collections.Hashtable] $Index ) Process { - $meta = $Manifest.Pages.$Title + $pageMeta = Get-PageMeta ` + -Host $Host ` + -Space $Space ` + -Title $Title ` + -Manifest $Manifest ` + -Index $Index - if (-Not $meta.Ref) + if (-Not $pageMeta.Ref) { throw "no reference to local content for page `$Title`." } - $content = Get-Content -Path $meta.Ref + $content = Get-Content -Path $pageMeta.Ref $transportBody = @{ 'type' = 'page' @@ -250,11 +75,13 @@ function New-Page { $response = ($rawResponse.Content | ConvertFrom-JSON) - $meta.PageId = $response.Id - - $meta.Version = $response.version | Select -ExpandProperty 'number' - - $meta + Update-PageMeta ` + -Title $Title ` + -Id $response.Id ` + -Version $response.version.number ` + -Hash (Get-StringHash $content).Hash ` + -Manifest $Manifest ` + -Index $Index } } @@ -280,30 +107,36 @@ function Update-Page [Parameter(Mandatory)] [string] $Space, # title of page to be published [Parameter(Mandatory)] [string] $Title, - # manifest - [Parameter(Mandatory)] [Collections.Hashtable] $Meta + # pages manifest + [Parameter(Mandatory)] [Array] $Manifest, + # pages manifest index + [Parameter()] [Collections.Hashtable] $Index ) Process { - $meta = $Manifest.Pages.$Title + $pageMeta = Get-PageMeta ` + -Host $Host ` + -Space $Space ` + -Title $Title ` + -Manifest $Manifest ` + -Index $Index - if (-Not $meta.Ref) + if (-Not $pageMeta.Ref) { throw "no reference to local content for page '$Title'." } - if (-Not $meta.Id) + if (-Not $pageMeta.Id) { throw "no id for page '$Title'." } - $content = Get-Content -Path $meta.Ref + $content = Get-Content -Path $pageMeta.Ref - #FIXME: create a stream instead of reading from filesystem again - $hash = (Get-FileHash -Path $meta.Ref -Algorithm SHA256).Hash + $hash = (Get-StringHash $content).Hash - if ($hash -eq $meta.Hash) + if ($hash -eq $pageMeta.Hash) { Write-Host "content unchanged, skipping: '$Title'" @@ -316,10 +149,10 @@ function Update-Page # we're not updating this in place, so that we don't have to reset the # value opon failure - $version = $meta.Version + 1 + $version = $pageMeta.Version + 1 $transportBody = @{ - 'id' = $PageId + 'id' = $PageMeta.Id 'type' = 'page' 'title' = $Title 'space' = @{ @@ -327,7 +160,7 @@ function Update-Page } 'body' = @{ 'storage' = @{ - 'value' = $Content + 'value' = $content 'representation' = 'storage' } } @@ -337,7 +170,7 @@ function Update-Page } | ConvertTo-JSON Invoke-WebRequest ` - -Uri "https://${Host}/rest/api/content/$PageId" ` + -Uri "https://${Host}/rest/api/content/$($PageMeta.Id)" ` -Method 'Put' ` -Headers @{ 'Authorization' = "Bearer $(Get-PersonalAccessToken $Host)" @@ -351,11 +184,13 @@ function Update-Page { $response = ($rawResponse.Content | ConvertFrom-JSON) - $meta.Version = $response.version | Select -ExpandProperty 'number' - - $meta.Hash = $hash - - $meta + Update-PageMeta ` + -Title $Title ` + -Id $pageMeta.Id ` + -Version $response.version.number ` + -Hash $hash ` + -Manifest $Manifest ` + -Index $Index } } @@ -439,8 +274,6 @@ function Publish-Page continue } } - - } - + } } } diff --git a/PSConfluencePublisher/PageMeta.Tests.ps1 b/PSConfluencePublisher/PageMeta.Tests.ps1 index fed27bc..1202a16 100755 --- a/PSConfluencePublisher/PageMeta.Tests.ps1 +++ b/PSConfluencePublisher/PageMeta.Tests.ps1 @@ -171,24 +171,24 @@ Describe 'Get-PageMeta' ` -Title 'foobar' ` -Space 'foobar' ` -Manifest @{'Pages'= {}} - } | Should -Throw + } | Should -Throw 'more than one result for query*' } It 'throws an exception, if there is no result' ` { - Mock Invoke-WebRequest { + Mock -ModuleName 'PageMeta' Invoke-WebRequest { @{ 'Content' = '{"results": []}' } } - { - Get-PageMeta ` - -Host 'confluence.contoso.com' ` - -Title 'foobar' ` - -Space 'foobar' ` - -Manifest @{'Pages'= {}} - } | Should -Throw + $result = Get-PageMeta ` + -Host 'confluence.contoso.com' ` + -Title 'foobar' ` + -Space 'foobar' ` + -Manifest @{'Pages'= {}} + + $result | Should -Be $null } } }