feat(Page): add tests

refactor(Page): convert functions to be usable with pipelines

this isn't complete though, will finish this tomorrow
This commit is contained in:
Rodweil, Theodor 2023-08-06 04:10:36 +02:00
parent 3882b1089e
commit 40c988ee45
No known key found for this signature in database
GPG key ID: F8BC1B0EB1F9CCF5
2 changed files with 635 additions and 181 deletions

View file

@ -0,0 +1,443 @@
#!/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' `
{
It 'fails, if page meta index does not exist' `
{
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
}
}
}
}
Describe 'Update-Page' `
{
Context 'Parameterized' `
{
It 'succeeds' `
{
InModuleScope Page `
{
$mockManifest = @{
'Pages' = @{
'mockTitle' = @{
'Ref' = 'pages/320okffs.xml'
'Id' = '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
}
}
It 'skips, if hash unchanged' `
{
InModuleScope Page `
{
$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
}
}
It 'fails, if page meta has no id' `
{
InModuleScope Page `
{
$mockManifest = @{
'Pages' = @{
'mockTitle' = @{
'Ref' = 'pages/320okffs.xml'
}
}
}
{
Update-Page `
-Host 'confluence.contoso.com' `
-Space 'testitest' `
-Title 'mockTitle' `
-Manifest $mockManifest
} | Should -Throw
}
}
}
}

View file

@ -10,42 +10,46 @@ function Get-CachedPageMeta
.EXAMPLE
Get-CachedPageMeta `
-Title 'd231cc3422bfdf96.xml' `
-CacheIndexFile 'confluence-page-cache.json'
-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)] [string] $CacheIndexFile
[Parameter(Mandatory)] [Collections.Hashtable] $Manifest
)
Process
{
try
{
$raw = Get-Content $CacheIndexFile
}
catch
{
$raw = "{}"
}
$data = $raw | ConvertFrom-JSON
#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 = $data | Select -ExpandProperty $Title
$pageMeta
$pageMeta = $pages | Select -ExpandProperty $Title
Write-Debug "page id cache hit: $Title -> $($pageMeta.PageId)"
$pageMeta
}
catch
{
$null
Write-Debug "page id cache miss: $Title"
$null
}
}
}
@ -63,17 +67,17 @@ function Get-PageMeta
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'
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)] [string] $CacheIndexFile
[Parameter(Mandatory)] [Collections.Hashtable] $Manifest
)
Process
@ -82,7 +86,7 @@ function Get-PageMeta
{
$cachedPageMeta = Get-CachedPageMeta `
-Title $Title `
-CacheIndexFile $CacheIndexFile
-Manifest $Manifest
}
if ($cachedPageMeta)
@ -92,17 +96,16 @@ function Get-PageMeta
$escapedTitle = [uri]::EscapeDataString($Title)
#TODO: move this to a separate function
$query = "title=${escapedTitle}&spaceKey=${Space}&expand=history"
Assert-PersonalAccessToken $Host
Invoke-WebRequest `
-Uri "https://${Host}/rest/api/content?$query" `
-Method 'Get' `
-Headers @{
'Authorization' = "Bearer $([System.Net.NetworkCredential]::new('', $script:PATS[$Host_]).Password)"
'Authorization' = "Bearer $(Get-PersonalAccessToken $Host)"
} `
-OutVariable response
-OutVariable response | Out-Null
$results = ($response.Content | ConvertFrom-JSON).results
@ -110,19 +113,20 @@ function Get-PageMeta
{
throw "more than one result for query: $query"
}
elseif ($results.Count -eq 1)
{
Register-PageMeta `
Update-PageMeta `
-PageId $results[0].id `
-Version ($results[0]._expandable | Select -ExpandProperty 'version') `
-Title $Title `
-CacheIndexFile $CacheIndexFile
-Manifest $Manifest
}
}
}
function Register-PageMeta
function Update-PageMeta
{
<#
.SYNOPSIS
@ -138,39 +142,44 @@ function Register-PageMeta
-Content @{}
#>
Param(
[Parameter(Mandatory)] [string] $PageId,
[Parameter()] [int] $Version = 0,
[Parameter(Mandatory)] [string] $Title,
[Parameter()] [string] $ContentHash = '',
[Parameter(Mandatory)] [string] $CacheIndexFile
[Parameter(Mandatory)] [string] $PageId,
[Parameter()] [int] $Version,
[Parameter()] [string] $AncestorTitle,
[Parameter()] [string] $Hash,
[Parameter(Mandatory)] [Collections.Hashtable] $Manifest
)
Process
{
try
$metaPages = $Manifest.Pages
if ((-Not $metaPages) -Or (-Not $metaPages.$Title))
{
$raw = Get-Content $CacheIndexFile
throw "page titled `$Title` not indexed in Manifest."
}
catch
{
$raw = "{}"
$meta = $metaPages.$Title
$meta['PageId'] = $PageId
if ($Version)
{
$meta['Version'] = $Version
}
$data = $raw | ConvertFrom-JSON
if ($AncestorTitle)
{
$meta['AncestorTitle'] = $AncestorTitle
}
$data | Add-Member -Name $Title `
-Value @{
'PageId' = $PageId
'Version' = $Version
'ContentHash' = $ContentHash
} `
-MemberType NoteProperty `
-Force
# if content didn't update, hash stays the same
if ($Hash)
{
$meta['Hash'] = $Hash
}
Set-Content -Path $CacheIndexFile -Value ($data | ConvertTo-JSON)
Write-Debug "indexed page id: $Title -> $PageId"
Write-Debug "register: $Title -> $PageId"
}
}
@ -184,27 +193,33 @@ function New-Page
.DESCRIPTION
.EXAMPLE
Add-ConfluencePage
Add-ConfluencePage `
-Host 'confluence.contoso.com' `
-Space 'TIARA' `
-Title 'Testitest' `
-Content @{}
#>
Param(
# confluence instance hostname
[Parameter(Mandatory)] [string] $Host,
# The name of the Confluence space to publish to
# name of the Confluence space to publish to
[Parameter(Mandatory)] [string] $Space,
# title of page to be published
[Parameter(Mandatory)] [string] $Title,
# content of page
[Parameter(Mandatory)] [string] $Content,
# parent page id
[Parameter()] [string] $Ancestor
# manifest
[Parameter(Mandatory)] [Collections.Hashtable] $Manifest
)
Process
{
Assert-PersonalAccessToken $Host
$meta = $Manifest.Pages.$Title
if (-Not $meta.Ref)
{
throw "no reference to local content for page `$Title`."
}
$content = Get-Content -Path $meta.Ref
$transportBody = @{
'type' = 'page'
@ -214,7 +229,7 @@ function New-Page
}
'body' = @{
'storage' = @{
'value' = $Content
'value' = $content
'representation' = 'storage'
}
}
@ -224,7 +239,7 @@ function New-Page
-Uri "https://${Host}/rest/api/content" `
-Method 'Post' `
-Headers @{
'Authorization' = "Bearer $([System.Net.NetworkCredential]::new('', $script:PATS[$Host_]).Password)"
'Authorization' = "Bearer $(Get-PersonalAccessToken $Host)"
} `
-ContentType "application/json" `
-Body $transportBody `
@ -235,10 +250,11 @@ function New-Page
{
$response = ($rawResponse.Content | ConvertFrom-JSON)
@{
'PageId' = $response.Id
'Version' = $response.version | Select -ExpandProperty 'number'
}
$meta.PageId = $response.Id
$meta.Version = $response.version | Select -ExpandProperty 'number'
$meta
}
}
@ -252,31 +268,55 @@ function Update-Page
.DESCRIPTION
.EXAMPLE
Add-ConfluencePage
Update-ConfluencePage
-Host 'confluence.contoso.com' `
-Space 'TIARA' `
-Title 'Testitest' `
-Content @{}
-Manifest @{}
#>
Param(
[Parameter(Mandatory)] [string] $Host,
# The page id of an existing page
[Parameter(Mandatory)] [string] $PageId,
# The name of the Confluence space to publish to
[Parameter(Mandatory)] [string] $Space,
# title of page to be published
[Parameter(Mandatory)] [string] $Title,
# version of content
[Parameter(Mandatory)] [int] $Version,
# content of page
[Parameter(Mandatory)] [string] $Content,
# parent page id
[Parameter()] [string] $Ancestor
# manifest
[Parameter(Mandatory)] [Collections.Hashtable] $Meta
)
Process
{
Assert-PersonalAccessToken $Host
$meta = $Manifest.Pages.$Title
if (-Not $meta.Ref)
{
throw "no reference to local content for page '$Title'."
}
if (-Not $meta.Id)
{
throw "no id for page '$Title'."
}
$content = Get-Content -Path $meta.Ref
#FIXME: create a stream instead of reading from filesystem again
$hash = (Get-FileHash -Path $meta.Ref -Algorithm SHA256).Hash
if ($hash -eq $meta.Hash)
{
Write-Host "content unchanged, skipping: '$Title'"
# yep, this is funny... This behaves like a return statement, because
# a cmdlet, treats the input as an array of inputs. We keep it that
# way so that all functions can properly act upon pipes. See
# additional information on 'Process' blocks.
continue
}
# we're not updating this in place, so that we don't have to reset the
# value opon failure
$version = $meta.Version + 1
$transportBody = @{
'id' = $PageId
@ -292,7 +332,7 @@ function Update-Page
}
}
'version' = @{
'number' = $Version
'number' = $version
}
} | ConvertTo-JSON
@ -300,7 +340,7 @@ function Update-Page
-Uri "https://${Host}/rest/api/content/$PageId" `
-Method 'Put' `
-Headers @{
'Authorization' = "Bearer $([System.Net.NetworkCredential]::new('', $script:PATS[$Host_]).Password)"
'Authorization' = "Bearer $(Get-PersonalAccessToken $Host)"
} `
-ContentType "application/json" `
-Body $transportBody `
@ -310,6 +350,12 @@ function Update-Page
End
{
$response = ($rawResponse.Content | ConvertFrom-JSON)
$meta.Version = $response.version | Select -ExpandProperty 'number'
$meta.Hash = $hash
$meta
}
}
@ -324,112 +370,77 @@ function Publish-Page
# name of Confluence space
[Parameter(Mandatory)] [string] $Space,
# manifest object
[Parameter(Mandatory)] [PSObject] $Manifest
[Parameter(Mandatory, ValueFromPipeline)] [PSObject] $Meta
)
Begin
Process
{
$pageMeta = Get-PageMeta `
ForEach($meta in $Meta)
{
$meta = Get-PageMeta `
-Host $hostname `
-Space $spaceName `
-Title $Title `
-Manifest $Manifest
if ($meta.AncestorTitle)
{
$ancestorPageMeta = Get-PageMeta `
-Host $hostname `
-Space $spaceName `
-Title $pageMeta.AncestorTitle `
-Manifest $Manifest
if (-Not ($ancestorPageMeta -Or $ancestorPageMeta.PageId))
{
Write-Host "ancestor, not published, skipping: $Title"
continue
}
}
if (-Not $pageId)
{
Write-Host ("create ${_}: $prettyName")
try {
New-Page `
-Host $hostname `
-Space $spaceName `
-Title $Title `
-Manifest $Manifest
}
catch
{
Write-Host "error for '$Title', skipping: $_"
continue
}
}
else
{
Write-Host ("update ${_} (${pageId}): $prettyName")
try
{
Update-Page `
-Host $hostname `
-Space $Space `
-Title $Title `
-Manifest $Manifest
}
catch
{
Write-Host "error for '$Title', skipping: $_"
continue
}
}
}
}
Process
{
if ($pageMeta.ContentHash -eq $_)
{
Write-Host "skipping (no changes): $Title"
return
}
$pageId = $pageMeta.PageId
$path = Join-Path $basepath 'content' "$_"
$pageContent = Get-Content $path | Out-String
$prettyName = $Title
if ($data.pages[$_].ancestor_id)
{
$ancestorTitle = $data.pages[$data.pages[$_].ancestor_id].title
$ancestorPageMeta = Get-PageMeta `
-Host $hostname `
-Space $spaceName `
-Title $ancestorTitle `
-CacheIndexFile $cacheIndexFile
if ($ancestorPageMeta)
{
$ancestorPageId = $ancestorPageMeta.PageId
}
$prettyName += " [$ancestorPageId]"
}
if (-Not $pageId)
{
Write-Host ("create ${_}: $prettyName")
try {
$pageMeta = New-Page `
-Host $hostname `
-Space $spaceName `
-Title $pageTitle `
-Content $pageContent `
-Ancestor $ancestorPageId
}
catch
{
Write-Host "error (skipping): $prettyName"
return
}
Register-PageMeta `
-PageId $pageMeta.PageId `
-Version $pageMeta.Version `
-Title $pageTitle `
-ContentHash $_ `
-CacheIndexFile $cacheIndexFile
}
else
{
Write-Host ("update ${_} (${pageId}): $prettyName")
$version = $pageMeta.Version + 1
try
{
Update-Page `
-Host $hostname `
-PageId $pageId `
-Space $spaceName `
-Title $pageTitle `
-Version $version `
-Content $pageContent `
-Ancestor $ancestorPageId
}
catch
{
Write-Host "error (skipping): $prettyName"
return
}
Register-PageMeta `
-PageId $pageMeta.PageId `
-Version $version `
-Title $pageTitle `
-ContentHash $_ `
-CacheIndexFile $cacheIndexFile
}
}
}
}