refactor(Page): add test suite and adapt to manifest schema

This commit is contained in:
Rodweil, Theodor 2023-08-06 22:28:49 +02:00
parent 1cebecaed1
commit 52369d540d
No known key found for this signature in database
GPG key ID: F8BC1B0EB1F9CCF5
3 changed files with 270 additions and 621 deletions

View file

@ -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*"
}
}
}

View file

@ -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
}
}
}
}
}
}

View file

@ -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
}
}
}