From 21282d9aebbf099abd14a7c59a4603663a8cf8cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:42:41 +0000 Subject: [PATCH 01/49] Bump PSModule/Process-PSModule/.github/workflows/workflow.yml Bumps [PSModule/Process-PSModule/.github/workflows/workflow.yml](https://github.com/psmodule/process-psmodule) from 5.4.6 to 5.5.7. - [Release notes](https://github.com/psmodule/process-psmodule/releases) - [Commits](https://github.com/psmodule/process-psmodule/compare/205d193f34cbbaf9992955c21d842bcf98a1859f...ce64918acc96dda73eb78f827036b794bfa6fa1a) --- updated-dependencies: - dependency-name: PSModule/Process-PSModule/.github/workflows/workflow.yml dependency-version: 5.5.7 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/Process-PSModule.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml index c61da8e..ee33000 100644 --- a/.github/workflows/Process-PSModule.yml +++ b/.github/workflows/Process-PSModule.yml @@ -27,6 +27,6 @@ permissions: jobs: Process-PSModule: - uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@205d193f34cbbaf9992955c21d842bcf98a1859f # v5.4.6 + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@ce64918acc96dda73eb78f827036b794bfa6fa1a # v5.5.7 secrets: APIKEY: ${{ secrets.APIKEY }} From c11d1b8f2fadf3522c8b27eb2cb7ecaf838956dd Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 6 Jul 2026 01:31:37 +0200 Subject: [PATCH 02/49] Fix README placeholders --- README.md | 68 ++++--------------------------------------------------- 1 file changed, 5 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 6319793..7a3847e 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,11 @@ -# {{ NAME }} +# Domeneshop -{{ DESCRIPTION }} +Domeneshop is intended to be a PowerShell module for interacting with the Domeneshop APIs. -## Prerequisites +## Status -This uses the following external resources: -- The [PSModule framework](https://github.com/PSModule/Process-PSModule) for building, testing and publishing the module. - -## Installation - -To install the module from the PowerShell Gallery, you can use the following command: - -```powershell -Install-PSResource -Name {{ NAME }} -Import-Module -Name {{ NAME }} -``` - -## Usage - -Here is a list of example that are typical use cases for the module. - -### Example 1: Greet an entity - -Provide examples for typical commands that a user would like to do with the module. - -```powershell -Greet-Entity -Name 'World' -Hello, World! -``` - -### Example 2 - -Provide examples for typical commands that a user would like to do with the module. - -```powershell -Import-Module -Name PSModuleTemplate -``` - -### Find more examples - -To find more examples of how to use the module, please refer to the [examples](examples) folder. - -Alternatively, you can use the Get-Command -Module 'This module' to find more commands that are available in the module. -To find examples of each of the commands you can use Get-Help -Examples 'CommandName'. - -## Documentation - -Link to further documentation if available, or describe where in the repository users can find more detailed documentation about -the module's functions and features. +This repository is currently a placeholder. The module source still contains scaffold code, so there are no supported commands or usage examples to document yet. ## Contributing -Coder or not, you can contribute to the project! We welcome all contributions. - -### For Users - -If you don't code, you still sit on valuable information that can make this project even better. If you experience that the -product does unexpected things, throw errors or is missing functionality, you can help by submitting bugs and feature requests. -Please see the issues tab on this project and submit a new issue that matches your needs. - -### For Developers - -If you do code, we'd love to have your contributions. Please read the [Contribution guidelines](CONTRIBUTING.md) for more information. -You can either help by picking up an existing issue or submit a new one if you have an idea for a new feature or improvement. - -## Acknowledgements - -Here is a list of people and projects that helped this project in some way. +Issues and pull requests are welcome when implementation work begins. From 35a5535a47e2543c5003129e2709281e6d637bfa Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 6 Jul 2026 08:26:46 +0200 Subject: [PATCH 03/49] Standardize README landing page --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7a3847e..6336c07 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ Domeneshop is intended to be a PowerShell module for interacting with the Domene This repository is currently a placeholder. The module source still contains scaffold code, so there are no supported commands or usage examples to document yet. +## Documentation + +When this module is implemented, command details should live in PowerShell help and generated documentation rather than being duplicated in this README. + ## Contributing Issues and pull requests are welcome when implementation work begins. From d47c0c58c497fd1ff3ad312158a7f358512da5df Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Tue, 7 Jul 2026 17:45:03 +0200 Subject: [PATCH 04/49] Require Pester 6.x in test files --- tests/PSModuleTest.Tests.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/PSModuleTest.Tests.ps1 b/tests/PSModuleTest.Tests.ps1 index 8258bb7..6b56f54 100644 --- a/tests/PSModuleTest.Tests.ps1 +++ b/tests/PSModuleTest.Tests.ps1 @@ -1,4 +1,6 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*'; GUID = 'a699dea5-2c73-4616-a270-1f7abb777e71' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester tests' )] From 2f02acd8fc7cf09f98efc10cf883b98a19ee1955 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Tue, 7 Jul 2026 21:11:15 +0200 Subject: [PATCH 05/49] Drop the GUID from the Pester requirement The GUID pins module identity (precise pinning), a stricter control than the lock-to-major risk appetite. Keep only the version range. --- tests/PSModuleTest.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PSModuleTest.Tests.ps1 b/tests/PSModuleTest.Tests.ps1 index 6b56f54..ff69bb3 100644 --- a/tests/PSModuleTest.Tests.ps1 +++ b/tests/PSModuleTest.Tests.ps1 @@ -1,4 +1,4 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*'; GUID = 'a699dea5-2c73-4616-a270-1f7abb777e71' } +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', From 6a28bc45bdb8133b75ea5b36214b5e27124dd649 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 12 Jul 2026 11:54:40 +0200 Subject: [PATCH 06/49] Adopt Process-PSModule v6.1.4 --- .github/workflows/Process-PSModule.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml index c61da8e..23b436c 100644 --- a/.github/workflows/Process-PSModule.yml +++ b/.github/workflows/Process-PSModule.yml @@ -27,6 +27,6 @@ permissions: jobs: Process-PSModule: - uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@205d193f34cbbaf9992955c21d842bcf98a1859f # v5.4.6 + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@da180bac16b13bfbcdf08b2e4e221b5b49e5ff28 # v6.1.4 secrets: APIKEY: ${{ secrets.APIKEY }} From 23a4a98a5f3cfb9a32c883a61810bb4431f508ec Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 12 Jul 2026 12:32:58 +0200 Subject: [PATCH 07/49] =?UTF-8?q?=F0=9F=93=96=20[Docs]:=20Remove=20Contrib?= =?UTF-8?q?uting=20section=20from=20placeholder=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 6336c07..ac5bbae 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,3 @@ This repository is currently a placeholder. The module source still contains sca ## Documentation When this module is implemented, command details should live in PowerShell help and generated documentation rather than being duplicated in this README. - -## Contributing - -Issues and pull requests are welcome when implementation work begins. From eb063ece9103cb4ec4f157ff1c6e4738a03112ad Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 12 Jul 2026 12:37:25 +0200 Subject: [PATCH 08/49] =?UTF-8?q?=F0=9F=93=96=20[Docs]:=20Clarify=20docume?= =?UTF-8?q?ntation=20is=20not=20published=20yet=20for=20placeholder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac5bbae..78b6dd6 100644 --- a/README.md +++ b/README.md @@ -8,4 +8,4 @@ This repository is currently a placeholder. The module source still contains sca ## Documentation -When this module is implemented, command details should live in PowerShell help and generated documentation rather than being duplicated in this README. +Documentation is not published yet because this module is still a placeholder. Once the module is implemented, command details will live in PowerShell help and generated documentation rather than being duplicated in this README. From 6bc7a36ad9da7fe2a7e00c93fb3f4574de40c759 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 16:53:13 +0200 Subject: [PATCH 09/49] Add Domeneshop context auth flow Store Domeneshop API credentials with Context and add a first authenticated domains command with a shared request helper. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../private/Auth/Get-DomeneshopConfig.ps1 | 19 +++++++ .../Auth/Resolve-DomeneshopContext.ps1 | 23 ++++++++ .../Auth/Set-DomeneshopDefaultContext.ps1 | 11 ++++ .../private/Invoke-DomeneshopApiRequest.ps1 | 37 ++++++++++++ .../public/Auth/Connect-DomeneshopAccount.ps1 | 56 +++++++++++++++++++ .../public/Auth/Get-DomeneshopContext.ps1 | 35 ++++++++++++ .../public/Domains/Get-DomeneshopDomain.ps1 | 38 +++++++++++++ 7 files changed, 219 insertions(+) create mode 100644 src/functions/private/Auth/Get-DomeneshopConfig.ps1 create mode 100644 src/functions/private/Auth/Resolve-DomeneshopContext.ps1 create mode 100644 src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 create mode 100644 src/functions/private/Invoke-DomeneshopApiRequest.ps1 create mode 100644 src/functions/public/Auth/Connect-DomeneshopAccount.ps1 create mode 100644 src/functions/public/Auth/Get-DomeneshopContext.ps1 create mode 100644 src/functions/public/Domains/Get-DomeneshopDomain.ps1 diff --git a/src/functions/private/Auth/Get-DomeneshopConfig.ps1 b/src/functions/private/Auth/Get-DomeneshopConfig.ps1 new file mode 100644 index 0000000..4e51fd2 --- /dev/null +++ b/src/functions/private/Auth/Get-DomeneshopConfig.ps1 @@ -0,0 +1,19 @@ +function Get-DomeneshopConfig { + [CmdletBinding()] + param() + + $vault = 'Domeneshop' + $id = '__Domeneshop.Config' + $config = Get-Context -ID $id -Vault $vault -ErrorAction SilentlyContinue + + if (-not $config) { + $config = [ordered]@{ + DefaultContext = $null + ApiBaseUri = 'https://api.domeneshop.no/v0' + } + Set-Context -ID $id -Vault $vault -Context $config + $config = Get-Context -ID $id -Vault $vault + } + + $config +} diff --git a/src/functions/private/Auth/Resolve-DomeneshopContext.ps1 b/src/functions/private/Auth/Resolve-DomeneshopContext.ps1 new file mode 100644 index 0000000..e3aeca2 --- /dev/null +++ b/src/functions/private/Auth/Resolve-DomeneshopContext.ps1 @@ -0,0 +1,23 @@ +function Resolve-DomeneshopContext { + [CmdletBinding()] + param( + [Parameter()] + [string] $Context + ) + + $resolvedContext = if ($Context) { + Get-DomeneshopContext -Context $Context + } else { + Get-DomeneshopContext + } + + if (-not $resolvedContext) { + throw "No Domeneshop context found. Run 'Connect-DomeneshopAccount' first." + } + + if ([string]::IsNullOrEmpty($resolvedContext.Token) -or -not ($resolvedContext.Secret -is [securestring])) { + throw "The Domeneshop context [$($resolvedContext.ID)] is missing valid credentials." + } + + $resolvedContext +} diff --git a/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 b/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 new file mode 100644 index 0000000..a256e6e --- /dev/null +++ b/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 @@ -0,0 +1,11 @@ +function Set-DomeneshopDefaultContext { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Context + ) + + $config = Get-DomeneshopConfig + $config.DefaultContext = $Context + Set-Context -ID '__Domeneshop.Config' -Vault 'Domeneshop' -Context $config +} diff --git a/src/functions/private/Invoke-DomeneshopApiRequest.ps1 b/src/functions/private/Invoke-DomeneshopApiRequest.ps1 new file mode 100644 index 0000000..cbc1baa --- /dev/null +++ b/src/functions/private/Invoke-DomeneshopApiRequest.ps1 @@ -0,0 +1,37 @@ +function Invoke-DomeneshopApiRequest { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateSet('Get', 'Post', 'Put', 'Delete')] + [string] $Method, + + [Parameter(Mandatory)] + [string] $Uri, + + [Parameter(Mandatory)] + [object] $Context, + + [Parameter()] + [AllowNull()] + [object] $Body + ) + + $credential = [pscredential]::new( + [string] $Context.Token, + [securestring] $Context.Secret + ) + + $params = @{ + Method = $Method + Uri = $Uri + Authentication = 'Basic' + Credential = $credential + } + + if ($PSBoundParameters.ContainsKey('Body')) { + $params['ContentType'] = 'application/json' + $params['Body'] = ($Body | ConvertTo-Json -Depth 100) + } + + Invoke-RestMethod @params +} diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 new file mode 100644 index 0000000..0a1ad41 --- /dev/null +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -0,0 +1,56 @@ +function Connect-DomeneshopAccount { + <# + .SYNOPSIS + Stores Domeneshop API credentials in a secure context. + + .DESCRIPTION + Stores Domeneshop API credentials using the Context module and optionally sets the context as default. + + .EXAMPLE + Connect-DomeneshopAccount -Token 'my-token' -Secret (Read-Host -AsSecureString) + #> + [OutputType([object])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Token, + + [Parameter(Mandatory)] + [object] $Secret, + + [Parameter()] + [string] $Context = 'default', + + [Parameter()] + [switch] $Default, + + [Parameter()] + [switch] $PassThru + ) + + $secureSecret = switch ($Secret) { + { $_ -is [securestring] } { $_; break } + { $_ -is [string] } { ConvertTo-SecureString -AsPlainText $Secret -Force; break } + default { throw 'Secret must be a SecureString or String value.' } + } + + $contextObject = [ordered]@{ + Name = $Context + Token = $Token + Secret = $secureSecret + AuthType = 'BasicAuth' + ApiBaseUri = 'https://api.domeneshop.no/v0' + ConnectedAt = Get-Date + } + + Set-Context -ID $Context -Vault 'Domeneshop' -Context $contextObject + + $config = Get-DomeneshopConfig + if ($Default -or [string]::IsNullOrEmpty($config.DefaultContext)) { + Set-DomeneshopDefaultContext -Context $Context + } + + if ($PassThru) { + Get-DomeneshopContext -Context $Context + } +} diff --git a/src/functions/public/Auth/Get-DomeneshopContext.ps1 b/src/functions/public/Auth/Get-DomeneshopContext.ps1 new file mode 100644 index 0000000..0bd6741 --- /dev/null +++ b/src/functions/public/Auth/Get-DomeneshopContext.ps1 @@ -0,0 +1,35 @@ +function Get-DomeneshopContext { + <# + .SYNOPSIS + Gets stored Domeneshop contexts. + + .DESCRIPTION + Returns one or more contexts stored in the Domeneshop context vault. + + .EXAMPLE + Get-DomeneshopContext + #> + [OutputType([object])] + [CmdletBinding(DefaultParameterSetName = 'GetDefault')] + param( + [Parameter(Mandatory, ParameterSetName = 'GetNamed')] + [string] $Context, + + [Parameter(Mandatory, ParameterSetName = 'ListAvailable')] + [switch] $ListAvailable + ) + + $id = switch ($PSCmdlet.ParameterSetName) { + 'GetNamed' { $Context; break } + 'ListAvailable' { '*'; break } + default { + $config = Get-DomeneshopConfig + if ([string]::IsNullOrEmpty($config.DefaultContext)) { + throw "No default Domeneshop context found. Run 'Connect-DomeneshopAccount' first." + } + $config.DefaultContext + } + } + + Get-Context -ID $id -Vault 'Domeneshop' | Where-Object { $_.ID -ne '__Domeneshop.Config' } | Sort-Object -Property ID +} diff --git a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 new file mode 100644 index 0000000..e56d55c --- /dev/null +++ b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 @@ -0,0 +1,38 @@ +function Get-DomeneshopDomain { + <# + .SYNOPSIS + Lists Domeneshop domains. + + .DESCRIPTION + Uses the stored Domeneshop context credentials and calls the /domains endpoint. + + .EXAMPLE + Get-DomeneshopDomain + + .EXAMPLE + Get-DomeneshopDomain -Domain '.no' + #> + [OutputType([object[]])] + [CmdletBinding()] + param( + [Parameter()] + [string] $Domain, + + [Parameter()] + [string] $Context + ) + + $resolvedContext = Resolve-DomeneshopContext -Context $Context + $apiBaseUri = [string] $resolvedContext.ApiBaseUri + if ([string]::IsNullOrEmpty($apiBaseUri)) { + $apiBaseUri = 'https://api.domeneshop.no/v0' + } + + $uri = "$apiBaseUri/domains" + if ($Domain) { + $encodedDomain = [uri]::EscapeDataString($Domain) + $uri = "${uri}?domain=$encodedDomain" + } + + Invoke-DomeneshopApiRequest -Method Get -Uri $uri -Context $resolvedContext +} From f32ed30a6aa00519cad09b0c230053391efdca73 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 16:53:16 +0200 Subject: [PATCH 10/49] Add tests for context credential workflow Load function scripts in test runtime and cover credential storage, default context behavior, domain query construction, and basic auth request execution. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/public/Get-PSModuleTest.ps1 | 4 +- tests/DomeneshopContext.Tests.ps1 | 78 +++++++++++++++++++++++ tests/PSModuleTest.Tests.ps1 | 7 ++ 3 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 tests/DomeneshopContext.Tests.ps1 diff --git a/src/functions/public/Get-PSModuleTest.ps1 b/src/functions/public/Get-PSModuleTest.ps1 index ffe3483..77483d0 100644 --- a/src/functions/public/Get-PSModuleTest.ps1 +++ b/src/functions/public/Get-PSModuleTest.ps1 @@ -1,6 +1,4 @@ -#Requires -Modules Utilities - -function Get-PSModuleTest { +function Get-PSModuleTest { <# .SYNOPSIS Performs tests on a module. diff --git a/tests/DomeneshopContext.Tests.ps1 b/tests/DomeneshopContext.Tests.ps1 new file mode 100644 index 0000000..81bb782 --- /dev/null +++ b/tests/DomeneshopContext.Tests.ps1 @@ -0,0 +1,78 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester tests' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester tests' +)] +[CmdletBinding()] +param() + +Describe 'Domeneshop context and auth flow' { + BeforeAll { + $functionFiles = Get-ChildItem -Path "$PSScriptRoot\..\src\functions" -Filter '*.ps1' -Recurse -File | Sort-Object -Property FullName + foreach ($file in $functionFiles) { + . $file.FullName + } + } + + It 'Connect-DomeneshopAccount stores credentials in Context and can set default' { + Mock Set-Context {} + Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = $null } } + Mock Set-DomeneshopDefaultContext {} + Mock Get-DomeneshopContext { [pscustomobject]@{ ID = 'demo' } } + + Connect-DomeneshopAccount -Token 'token' -Secret 'secret' -Context 'demo' -PassThru | Should -Not -BeNullOrEmpty + + Should -Invoke Set-Context -Times 1 -Exactly -ParameterFilter { + $ID -eq 'demo' -and + $Vault -eq 'Domeneshop' -and + $Context.Secret -is [securestring] + } + Should -Invoke Set-DomeneshopDefaultContext -Times 1 -Exactly + } + + It 'Get-DomeneshopContext throws when default context is not configured' { + Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = '' } } + + { Get-DomeneshopContext } | Should -Throw + } + + It 'Get-DomeneshopDomain calls domains endpoint with domain filter' { + Mock Resolve-DomeneshopContext { + [pscustomobject]@{ + ID = 'demo' + Token = 'token' + Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force + ApiBaseUri = 'https://api.domeneshop.no/v0' + } + } + Mock Invoke-DomeneshopApiRequest { @() } + + { Get-DomeneshopDomain -Context 'demo' -Domain '.no' } | Should -Not -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains?domain=.no' -and + $Context.ID -eq 'demo' + } + } + + It 'Invoke-DomeneshopApiRequest uses basic authentication' { + Mock Invoke-RestMethod { [pscustomobject]@{ ok = $true } } + $contextObject = [pscustomobject]@{ + Token = 'token' + Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force + } + + $result = Invoke-DomeneshopApiRequest -Method Get -Uri 'https://api.domeneshop.no/v0/domains' -Context $contextObject + + $result.ok | Should -BeTrue + Should -Invoke Invoke-RestMethod -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Authentication -eq 'Basic' -and + $Credential.UserName -eq 'token' + } + } +} diff --git a/tests/PSModuleTest.Tests.ps1 b/tests/PSModuleTest.Tests.ps1 index 8258bb7..fb9fdde 100644 --- a/tests/PSModuleTest.Tests.ps1 +++ b/tests/PSModuleTest.Tests.ps1 @@ -10,6 +10,13 @@ param() Describe 'Module' { + BeforeAll { + $functionFiles = Get-ChildItem -Path "$PSScriptRoot\..\src\functions" -Filter '*.ps1' -Recurse -File | Sort-Object -Property FullName + foreach ($file in $functionFiles) { + . $file.FullName + } + } + It 'Function: Get-PSModuleTest' { Get-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' } From 25200d180b6deb1471d0bc0062243cebddfc3bcc Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 17:39:57 +0200 Subject: [PATCH 11/49] Implement full Domeneshop v0 endpoint commands Add public cmdlets for domains by ID, DNS CRUD, forwards CRUD, DDNS update, and invoices list/get. Reuse Context-based auth for all requests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../private/Get-DomeneshopApiBaseUri.ps1 | 14 +++++ .../public/Ddns/Update-DomeneshopDdns.ps1 | 32 +++++++++++ .../public/Dns/Add-DomeneshopDnsRecord.ps1 | 24 ++++++++ .../public/Dns/Get-DomeneshopDnsRecord.ps1 | 55 +++++++++++++++++++ .../public/Dns/Remove-DomeneshopDnsRecord.ps1 | 26 +++++++++ .../public/Dns/Set-DomeneshopDnsRecord.ps1 | 27 +++++++++ .../public/Domains/Get-DomeneshopDomain.ps1 | 26 +++++---- .../public/Forwards/Add-DomeneshopForward.ps1 | 24 ++++++++ .../public/Forwards/Get-DomeneshopForward.ps1 | 34 ++++++++++++ .../Forwards/Remove-DomeneshopForward.ps1 | 28 ++++++++++ .../public/Forwards/Set-DomeneshopForward.ps1 | 29 ++++++++++ .../public/Invoices/Get-DomeneshopInvoice.ps1 | 29 ++++++++++ 12 files changed, 338 insertions(+), 10 deletions(-) create mode 100644 src/functions/private/Get-DomeneshopApiBaseUri.ps1 create mode 100644 src/functions/public/Ddns/Update-DomeneshopDdns.ps1 create mode 100644 src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 create mode 100644 src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 create mode 100644 src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 create mode 100644 src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 create mode 100644 src/functions/public/Forwards/Add-DomeneshopForward.ps1 create mode 100644 src/functions/public/Forwards/Get-DomeneshopForward.ps1 create mode 100644 src/functions/public/Forwards/Remove-DomeneshopForward.ps1 create mode 100644 src/functions/public/Forwards/Set-DomeneshopForward.ps1 create mode 100644 src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 diff --git a/src/functions/private/Get-DomeneshopApiBaseUri.ps1 b/src/functions/private/Get-DomeneshopApiBaseUri.ps1 new file mode 100644 index 0000000..8c538a0 --- /dev/null +++ b/src/functions/private/Get-DomeneshopApiBaseUri.ps1 @@ -0,0 +1,14 @@ +function Get-DomeneshopApiBaseUri { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [object] $Context + ) + + $apiBaseUri = [string] $Context.ApiBaseUri + if ([string]::IsNullOrEmpty($apiBaseUri)) { + $apiBaseUri = 'https://api.domeneshop.no/v0' + } + + $apiBaseUri.TrimEnd('/') +} diff --git a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 new file mode 100644 index 0000000..ea5970b --- /dev/null +++ b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 @@ -0,0 +1,32 @@ +function Update-DomeneshopDdns { + <# + .SYNOPSIS + Updates dynamic DNS for a hostname. + + .DESCRIPTION + Calls the Domeneshop DDNS update endpoint. The API creates the A/AAAA record if it does not exist. + #> + [OutputType([object])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [Alias('Host')] + [string] $Hostname, + + [Parameter()] + [Alias('IP')] + [string] $MyIP, + + [Parameter()] + [string] $Context + ) + + $resolvedContext = Resolve-DomeneshopContext -Context $Context + $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext + $uri = "$apiBaseUri/dyndns/update?hostname=$([uri]::EscapeDataString($Hostname))" + if ($PSBoundParameters.ContainsKey('MyIP')) { + $uri = "$uri&myip=$([uri]::EscapeDataString($MyIP))" + } + + Invoke-DomeneshopApiRequest -Method Get -Uri $uri -Context $resolvedContext +} diff --git a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 new file mode 100644 index 0000000..0277ae1 --- /dev/null +++ b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 @@ -0,0 +1,24 @@ +function Add-DomeneshopDnsRecord { + <# + .SYNOPSIS + Adds a DNS record to a Domeneshop domain. + #> + [OutputType([object])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [int] $DomainID, + + [Parameter(Mandatory)] + [object] $Record, + + [Parameter()] + [string] $Context + ) + + $resolvedContext = Resolve-DomeneshopContext -Context $Context + $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext + $uri = "$apiBaseUri/domains/$DomainID/dns" + + Invoke-DomeneshopApiRequest -Method Post -Uri $uri -Context $resolvedContext -Body $Record +} diff --git a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 new file mode 100644 index 0000000..d09e6e7 --- /dev/null +++ b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 @@ -0,0 +1,55 @@ +function Get-DomeneshopDnsRecord { + <# + .SYNOPSIS + Gets DNS records for a Domeneshop domain. + + .DESCRIPTION + Lists DNS records for a domain, or gets one DNS record by ID. + #> + [OutputType([object[]])] + [CmdletBinding(DefaultParameterSetName = 'List')] + param( + [Parameter(Mandatory)] + [int] $DomainID, + + [Parameter(Mandatory, ParameterSetName = 'GetByID')] + [int] $RecordID, + + [Parameter(ParameterSetName = 'List')] + [Alias('Host')] + [string] $RecordHost, + + [Parameter(ParameterSetName = 'List')] + [string] $Type, + + [Parameter(ParameterSetName = 'List')] + [string] $Data, + + [Parameter()] + [string] $Context + ) + + $resolvedContext = Resolve-DomeneshopContext -Context $Context + $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext + $uri = "$apiBaseUri/domains/$DomainID/dns" + + if ($PSCmdlet.ParameterSetName -eq 'GetByID') { + $uri = "$uri/$RecordID" + } else { + $queryParts = @() + if ($PSBoundParameters.ContainsKey('RecordHost')) { + $queryParts += "host=$([uri]::EscapeDataString($RecordHost))" + } + if ($PSBoundParameters.ContainsKey('Type')) { + $queryParts += "type=$([uri]::EscapeDataString($Type))" + } + if ($PSBoundParameters.ContainsKey('Data')) { + $queryParts += "data=$([uri]::EscapeDataString($Data))" + } + if ($queryParts.Count -gt 0) { + $uri = "${uri}?{0}" -f ($queryParts -join '&') + } + } + + Invoke-DomeneshopApiRequest -Method Get -Uri $uri -Context $resolvedContext +} diff --git a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 new file mode 100644 index 0000000..987476b --- /dev/null +++ b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 @@ -0,0 +1,26 @@ +function Remove-DomeneshopDnsRecord { + <# + .SYNOPSIS + Removes a DNS record from a Domeneshop domain. + #> + [OutputType([object])] + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] + param( + [Parameter(Mandatory)] + [int] $DomainID, + + [Parameter(Mandatory)] + [int] $RecordID, + + [Parameter()] + [string] $Context + ) + + $resolvedContext = Resolve-DomeneshopContext -Context $Context + $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext + $uri = "$apiBaseUri/domains/$DomainID/dns/$RecordID" + + if ($PSCmdlet.ShouldProcess("Domain $DomainID DNS record $RecordID", 'Remove')) { + Invoke-DomeneshopApiRequest -Method Delete -Uri $uri -Context $resolvedContext + } +} diff --git a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 new file mode 100644 index 0000000..6c13961 --- /dev/null +++ b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 @@ -0,0 +1,27 @@ +function Set-DomeneshopDnsRecord { + <# + .SYNOPSIS + Updates a DNS record on a Domeneshop domain. + #> + [OutputType([object])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [int] $DomainID, + + [Parameter(Mandatory)] + [int] $RecordID, + + [Parameter(Mandatory)] + [object] $Record, + + [Parameter()] + [string] $Context + ) + + $resolvedContext = Resolve-DomeneshopContext -Context $Context + $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext + $uri = "$apiBaseUri/domains/$DomainID/dns/$RecordID" + + Invoke-DomeneshopApiRequest -Method Put -Uri $uri -Context $resolvedContext -Body $Record +} diff --git a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 index e56d55c..be1ba64 100644 --- a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 +++ b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 @@ -13,25 +13,31 @@ function Get-DomeneshopDomain { Get-DomeneshopDomain -Domain '.no' #> [OutputType([object[]])] - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = 'List')] param( - [Parameter()] + [Parameter(ParameterSetName = 'List')] [string] $Domain, + [Parameter(Mandatory, ParameterSetName = 'GetByID')] + [int] $DomainID, + [Parameter()] [string] $Context ) $resolvedContext = Resolve-DomeneshopContext -Context $Context - $apiBaseUri = [string] $resolvedContext.ApiBaseUri - if ([string]::IsNullOrEmpty($apiBaseUri)) { - $apiBaseUri = 'https://api.domeneshop.no/v0' - } + $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext - $uri = "$apiBaseUri/domains" - if ($Domain) { - $encodedDomain = [uri]::EscapeDataString($Domain) - $uri = "${uri}?domain=$encodedDomain" + $uri = switch ($PSCmdlet.ParameterSetName) { + 'GetByID' { "$apiBaseUri/domains/$DomainID"; break } + default { + $listUri = "$apiBaseUri/domains" + if ($Domain) { + $encodedDomain = [uri]::EscapeDataString($Domain) + $listUri = "${listUri}?domain=$encodedDomain" + } + $listUri + } } Invoke-DomeneshopApiRequest -Method Get -Uri $uri -Context $resolvedContext diff --git a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 new file mode 100644 index 0000000..9f1e201 --- /dev/null +++ b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 @@ -0,0 +1,24 @@ +function Add-DomeneshopForward { + <# + .SYNOPSIS + Adds an HTTP forward to a Domeneshop domain. + #> + [OutputType([object])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [int] $DomainID, + + [Parameter(Mandatory)] + [object] $Forward, + + [Parameter()] + [string] $Context + ) + + $resolvedContext = Resolve-DomeneshopContext -Context $Context + $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext + $uri = "$apiBaseUri/domains/$DomainID/forwards/" + + Invoke-DomeneshopApiRequest -Method Post -Uri $uri -Context $resolvedContext -Body $Forward +} diff --git a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 new file mode 100644 index 0000000..c7a994f --- /dev/null +++ b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 @@ -0,0 +1,34 @@ +function Get-DomeneshopForward { + <# + .SYNOPSIS + Gets HTTP forwards for a Domeneshop domain. + + .DESCRIPTION + Lists forwards for a domain, or gets one forward by host. + #> + [OutputType([object[]])] + [CmdletBinding(DefaultParameterSetName = 'List')] + param( + [Parameter(Mandatory)] + [int] $DomainID, + + [Parameter(Mandatory, ParameterSetName = 'GetByHost')] + [Alias('Host')] + [string] $ForwardHost, + + [Parameter()] + [string] $Context + ) + + $resolvedContext = Resolve-DomeneshopContext -Context $Context + $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext + + $uri = if ($PSCmdlet.ParameterSetName -eq 'GetByHost') { + $encodedHost = [uri]::EscapeDataString($ForwardHost) + "$apiBaseUri/domains/$DomainID/forwards/$encodedHost" + } else { + "$apiBaseUri/domains/$DomainID/forwards/" + } + + Invoke-DomeneshopApiRequest -Method Get -Uri $uri -Context $resolvedContext +} diff --git a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 new file mode 100644 index 0000000..7e21f77 --- /dev/null +++ b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 @@ -0,0 +1,28 @@ +function Remove-DomeneshopForward { + <# + .SYNOPSIS + Removes an HTTP forward from a Domeneshop domain. + #> + [OutputType([object])] + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] + param( + [Parameter(Mandatory)] + [int] $DomainID, + + [Parameter(Mandatory)] + [Alias('Host')] + [string] $ForwardHost, + + [Parameter()] + [string] $Context + ) + + $resolvedContext = Resolve-DomeneshopContext -Context $Context + $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext + $encodedHost = [uri]::EscapeDataString($ForwardHost) + $uri = "$apiBaseUri/domains/$DomainID/forwards/$encodedHost" + + if ($PSCmdlet.ShouldProcess("Domain $DomainID forward $ForwardHost", 'Remove')) { + Invoke-DomeneshopApiRequest -Method Delete -Uri $uri -Context $resolvedContext + } +} diff --git a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 new file mode 100644 index 0000000..3901689 --- /dev/null +++ b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 @@ -0,0 +1,29 @@ +function Set-DomeneshopForward { + <# + .SYNOPSIS + Updates an HTTP forward on a Domeneshop domain. + #> + [OutputType([object])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [int] $DomainID, + + [Parameter(Mandatory)] + [Alias('Host')] + [string] $ForwardHost, + + [Parameter(Mandatory)] + [object] $Forward, + + [Parameter()] + [string] $Context + ) + + $resolvedContext = Resolve-DomeneshopContext -Context $Context + $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext + $encodedHost = [uri]::EscapeDataString($ForwardHost) + $uri = "$apiBaseUri/domains/$DomainID/forwards/$encodedHost" + + Invoke-DomeneshopApiRequest -Method Put -Uri $uri -Context $resolvedContext -Body $Forward +} diff --git a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 new file mode 100644 index 0000000..1a6495c --- /dev/null +++ b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 @@ -0,0 +1,29 @@ +function Get-DomeneshopInvoice { + <# + .SYNOPSIS + Gets Domeneshop invoices. + + .DESCRIPTION + Lists invoices, or gets a specific invoice by invoice number. + #> + [OutputType([object[]])] + [CmdletBinding(DefaultParameterSetName = 'List')] + param( + [Parameter(Mandatory, ParameterSetName = 'GetByID')] + [Alias('InvoiceNo')] + [int] $InvoiceID, + + [Parameter()] + [string] $Context + ) + + $resolvedContext = Resolve-DomeneshopContext -Context $Context + $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext + $uri = if ($PSCmdlet.ParameterSetName -eq 'GetByID') { + "$apiBaseUri/invoices/$InvoiceID" + } else { + "$apiBaseUri/invoices" + } + + Invoke-DomeneshopApiRequest -Method Get -Uri $uri -Context $resolvedContext +} From e1427779de2d41c00c83031b44e46c575abec0de Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 17:40:00 +0200 Subject: [PATCH 12/49] Expand tests for all API endpoint commands Cover URI/method construction for domains, DNS, forwards, invoices, and DDNS commands using mocked API invocations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/DomeneshopContext.Tests.ps1 | 158 ++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/tests/DomeneshopContext.Tests.ps1 b/tests/DomeneshopContext.Tests.ps1 index 81bb782..b42e7b2 100644 --- a/tests/DomeneshopContext.Tests.ps1 +++ b/tests/DomeneshopContext.Tests.ps1 @@ -75,4 +75,162 @@ Describe 'Domeneshop context and auth flow' { $Credential.UserName -eq 'token' } } + + It 'Get-DomeneshopDomain can get a domain by ID' { + Mock Resolve-DomeneshopContext { + [pscustomobject]@{ + ID = 'demo' + Token = 'token' + Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force + ApiBaseUri = 'https://api.domeneshop.no/v0' + } + } + Mock Invoke-DomeneshopApiRequest { @{} } + + { Get-DomeneshopDomain -Context 'demo' -DomainID 42 } | Should -Not -Throw + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42' + } + } + + It 'Get-DomeneshopDnsRecord builds list query and endpoint' { + Mock Resolve-DomeneshopContext { + [pscustomobject]@{ + ID = 'demo' + Token = 'token' + Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force + ApiBaseUri = 'https://api.domeneshop.no/v0' + } + } + Mock Invoke-DomeneshopApiRequest { @() } + + Get-DomeneshopDnsRecord -DomainID 9 -Host 'www' -Type 'A' -Data '127.0.0.1' + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/9/dns?host=www&type=A&data=127.0.0.1' + } + } + + It 'Get-DomeneshopDnsRecord can get record by ID' { + Mock Resolve-DomeneshopContext { + [pscustomobject]@{ + ID = 'demo' + Token = 'token' + Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force + ApiBaseUri = 'https://api.domeneshop.no/v0' + } + } + Mock Invoke-DomeneshopApiRequest { @{} } + + Get-DomeneshopDnsRecord -DomainID 9 -RecordID 3 + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/9/dns/3' + } + } + + It 'DNS mutating commands call expected endpoints' { + Mock Resolve-DomeneshopContext { + [pscustomobject]@{ + ID = 'demo' + Token = 'token' + Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force + ApiBaseUri = 'https://api.domeneshop.no/v0' + } + } + Mock Invoke-DomeneshopApiRequest { @{} } + + $record = @{ host = 'www'; type = 'A'; data = '127.0.0.1' } + Add-DomeneshopDnsRecord -DomainID 9 -Record $record + Set-DomeneshopDnsRecord -DomainID 9 -RecordID 3 -Record $record + Remove-DomeneshopDnsRecord -DomainID 9 -RecordID 3 -Confirm:$false + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Post' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/dns' + } + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Put' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/dns/3' + } + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Delete' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/dns/3' + } + } + + It 'Forward commands call expected endpoints' { + Mock Resolve-DomeneshopContext { + [pscustomobject]@{ + ID = 'demo' + Token = 'token' + Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force + ApiBaseUri = 'https://api.domeneshop.no/v0' + } + } + Mock Invoke-DomeneshopApiRequest { @{} } + + $forward = @{ host = 'www'; url = 'https://example.net' } + Get-DomeneshopForward -DomainID 9 + Get-DomeneshopForward -DomainID 9 -Host 'www' + Add-DomeneshopForward -DomainID 9 -Forward $forward + Set-DomeneshopForward -DomainID 9 -Host 'www' -Forward $forward + Remove-DomeneshopForward -DomainID 9 -Host 'www' -Confirm:$false + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/forwards/' + } + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/forwards/www' + } + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Post' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/forwards/' + } + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Put' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/forwards/www' + } + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Delete' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/forwards/www' + } + } + + It 'Get-DomeneshopInvoice supports list and by ID' { + Mock Resolve-DomeneshopContext { + [pscustomobject]@{ + ID = 'demo' + Token = 'token' + Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force + ApiBaseUri = 'https://api.domeneshop.no/v0' + } + } + Mock Invoke-DomeneshopApiRequest { @() } + + Get-DomeneshopInvoice + Get-DomeneshopInvoice -InvoiceID 7 + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and $Uri -eq 'https://api.domeneshop.no/v0/invoices' + } + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and $Uri -eq 'https://api.domeneshop.no/v0/invoices/7' + } + } + + It 'Update-DomeneshopDdns builds hostname and myip query' { + Mock Resolve-DomeneshopContext { + [pscustomobject]@{ + ID = 'demo' + Token = 'token' + Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force + ApiBaseUri = 'https://api.domeneshop.no/v0' + } + } + Mock Invoke-DomeneshopApiRequest { @{} } + + Update-DomeneshopDdns -Hostname 'home.example.com' -MyIP '1.2.3.4' + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and $Uri -eq 'https://api.domeneshop.no/v0/dyndns/update?hostname=home.example.com&myip=1.2.3.4' + } + } } From 0b812c052270131e69333a8278105f445a5fa911 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 18:05:38 +0200 Subject: [PATCH 13/49] Harden Domeneshop command contracts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../private/Auth/Get-DomeneshopConfig.ps1 | 37 +++++++++++-- .../Auth/Resolve-DomeneshopContext.ps1 | 51 ++++++++++++++---- .../Auth/Set-DomeneshopDefaultContext.ps1 | 39 +++++++++++++- .../private/Get-DomeneshopApiBaseUri.ps1 | 42 +++++++++++++-- .../private/Invoke-DomeneshopApiRequest.ps1 | 44 +++++++++++++++- .../public/Auth/Connect-DomeneshopAccount.ps1 | 50 +++++++++++++++--- .../public/Auth/Get-DomeneshopContext.ps1 | 52 ++++++++++++++----- .../public/Ddns/Update-DomeneshopDdns.ps1 | 46 ++++++++++++++-- .../public/Dns/Add-DomeneshopDnsRecord.ps1 | 45 ++++++++++++++-- .../public/Dns/Get-DomeneshopDnsRecord.ps1 | 49 +++++++++++++++-- .../public/Dns/Remove-DomeneshopDnsRecord.ps1 | 39 +++++++++++++- .../public/Dns/Set-DomeneshopDnsRecord.ps1 | 47 +++++++++++++++-- .../public/Domains/Get-DomeneshopDomain.ps1 | 35 +++++++++++-- .../public/Forwards/Add-DomeneshopForward.ps1 | 45 ++++++++++++++-- .../public/Forwards/Get-DomeneshopForward.ps1 | 43 +++++++++++++-- .../Forwards/Remove-DomeneshopForward.ps1 | 39 +++++++++++++- .../public/Forwards/Set-DomeneshopForward.ps1 | 47 +++++++++++++++-- .../public/Invoices/Get-DomeneshopInvoice.ps1 | 41 +++++++++++++-- tests/DomeneshopContext.Tests.ps1 | 16 +++--- 19 files changed, 722 insertions(+), 85 deletions(-) diff --git a/src/functions/private/Auth/Get-DomeneshopConfig.ps1 b/src/functions/private/Auth/Get-DomeneshopConfig.ps1 index 4e51fd2..0c0439f 100644 --- a/src/functions/private/Auth/Get-DomeneshopConfig.ps1 +++ b/src/functions/private/Auth/Get-DomeneshopConfig.ps1 @@ -1,18 +1,47 @@ +#Requires -Modules @{ ModuleName = 'Context'; ModuleVersion = '8.1.6' } + function Get-DomeneshopConfig { + <# + .SYNOPSIS + Get the Domeneshop module configuration. + + .DESCRIPTION + Read the module configuration from the Context vault or return an in-memory default when no configuration has been stored. + + .EXAMPLE + Get-DomeneshopConfig + + Get the current Domeneshop module configuration. + + .INPUTS + None + + You can't pipe objects to Get-DomeneshopConfig. + + .OUTPUTS + System.Object + + The stored or default Domeneshop module configuration. + + .NOTES + This read helper does not create or update Context vault entries. + + .LINK + https://psmodule.io/Context/ + #> + [OutputType([object])] [CmdletBinding()] param() $vault = 'Domeneshop' $id = '__Domeneshop.Config' - $config = Get-Context -ID $id -Vault $vault -ErrorAction SilentlyContinue + $config = Get-Context -ID $id -Vault $vault if (-not $config) { - $config = [ordered]@{ + $config = [pscustomobject][ordered]@{ DefaultContext = $null ApiBaseUri = 'https://api.domeneshop.no/v0' } - Set-Context -ID $id -Vault $vault -Context $config - $config = Get-Context -ID $id -Vault $vault } $config diff --git a/src/functions/private/Auth/Resolve-DomeneshopContext.ps1 b/src/functions/private/Auth/Resolve-DomeneshopContext.ps1 index e3aeca2..4042416 100644 --- a/src/functions/private/Auth/Resolve-DomeneshopContext.ps1 +++ b/src/functions/private/Auth/Resolve-DomeneshopContext.ps1 @@ -1,23 +1,52 @@ function Resolve-DomeneshopContext { + <# + .SYNOPSIS + Validate a resolved Domeneshop context. + + .DESCRIPTION + Verify that a context resolved by a public command contains the credentials and API endpoint required by private helpers. + + .EXAMPLE + Resolve-DomeneshopContext -Context $storedContext + + Validate and emit a stored Domeneshop context. + + .INPUTS + None + + You can't pipe objects to Resolve-DomeneshopContext. + + .OUTPUTS + System.Object + + The validated Domeneshop context. + + .NOTES + This helper never selects or defaults a context. Public commands own that behavior. + + .LINK + https://psmodule.io/Context/ + #> + [OutputType([object])] [CmdletBinding()] param( - [Parameter()] - [string] $Context + # The context object selected by a public command. + [Parameter(Mandatory)] + [AllowNull()] + [object] $Context ) - $resolvedContext = if ($Context) { - Get-DomeneshopContext -Context $Context - } else { - Get-DomeneshopContext + if ($null -eq $Context) { + throw "No Domeneshop context found. Run 'Connect-DomeneshopAccount' first." } - if (-not $resolvedContext) { - throw "No Domeneshop context found. Run 'Connect-DomeneshopAccount' first." + if ([string]::IsNullOrWhiteSpace([string] $Context.Token) -or -not ($Context.Secret -is [securestring])) { + throw "The Domeneshop context [$($Context.ID)] is missing valid credentials." } - if ([string]::IsNullOrEmpty($resolvedContext.Token) -or -not ($resolvedContext.Secret -is [securestring])) { - throw "The Domeneshop context [$($resolvedContext.ID)] is missing valid credentials." + if ([string]::IsNullOrWhiteSpace([string] $Context.ApiBaseUri)) { + throw "The Domeneshop context [$($Context.ID)] is missing an API base URI." } - $resolvedContext + $Context } diff --git a/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 b/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 index a256e6e..bfadda4 100644 --- a/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 +++ b/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 @@ -1,11 +1,46 @@ +#Requires -Modules @{ ModuleName = 'Context'; ModuleVersion = '8.1.6' } + function Set-DomeneshopDefaultContext { - [CmdletBinding()] + <# + .SYNOPSIS + Set the default Domeneshop context name. + + .DESCRIPTION + Persist the selected default context name in the Domeneshop configuration entry. + + .EXAMPLE + Set-DomeneshopDefaultContext -Context 'production' + + Set production as the default Domeneshop context. + + .INPUTS + None + + You can't pipe objects to Set-DomeneshopDefaultContext. + + .OUTPUTS + None + + Set-DomeneshopDefaultContext doesn't emit output. + + .NOTES + The public caller is responsible for guarding this mutation with ShouldProcess. + + .LINK + https://psmodule.io/Context/ + #> + [OutputType([void])] + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] param( + # The name of an existing Domeneshop context. [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] [string] $Context ) $config = Get-DomeneshopConfig $config.DefaultContext = $Context - Set-Context -ID '__Domeneshop.Config' -Vault 'Domeneshop' -Context $config + if ($PSCmdlet.ShouldProcess('Domeneshop module configuration', "Set [$Context] as the default context")) { + $null = Set-Context -ID '__Domeneshop.Config' -Vault 'Domeneshop' -Context $config + } } diff --git a/src/functions/private/Get-DomeneshopApiBaseUri.ps1 b/src/functions/private/Get-DomeneshopApiBaseUri.ps1 index 8c538a0..e8a58b3 100644 --- a/src/functions/private/Get-DomeneshopApiBaseUri.ps1 +++ b/src/functions/private/Get-DomeneshopApiBaseUri.ps1 @@ -1,14 +1,50 @@ function Get-DomeneshopApiBaseUri { + <# + .SYNOPSIS + Get the API base URI from a resolved Domeneshop context. + + .DESCRIPTION + Validate and normalize the API base URI stored on a resolved Domeneshop context. + + .EXAMPLE + Get-DomeneshopApiBaseUri -Context $resolvedContext + + Get the normalized API base URI for a resolved context. + + .INPUTS + None + + You can't pipe objects to Get-DomeneshopApiBaseUri. + + .OUTPUTS + System.String + + The normalized Domeneshop API base URI. + + .NOTES + The caller must resolve and validate the context before invoking this helper. + + .LINK + https://api.domeneshop.no/docs/ + #> + [OutputType([string])] [CmdletBinding()] param( + # The resolved Domeneshop context containing an API base URI. [Parameter(Mandatory)] + [ValidateNotNull()] [object] $Context ) $apiBaseUri = [string] $Context.ApiBaseUri - if ([string]::IsNullOrEmpty($apiBaseUri)) { - $apiBaseUri = 'https://api.domeneshop.no/v0' + if ([string]::IsNullOrWhiteSpace($apiBaseUri)) { + throw "The Domeneshop context [$($Context.ID)] does not define an API base URI." + } + + $parsedUri = $null + if (-not [uri]::TryCreate($apiBaseUri, [System.UriKind]::Absolute, [ref] $parsedUri)) { + throw "The Domeneshop context [$($Context.ID)] contains an invalid API base URI." } - $apiBaseUri.TrimEnd('/') + $parsedUri.AbsoluteUri.TrimEnd('/') } diff --git a/src/functions/private/Invoke-DomeneshopApiRequest.ps1 b/src/functions/private/Invoke-DomeneshopApiRequest.ps1 index cbc1baa..3cfd191 100644 --- a/src/functions/private/Invoke-DomeneshopApiRequest.ps1 +++ b/src/functions/private/Invoke-DomeneshopApiRequest.ps1 @@ -1,16 +1,56 @@ function Invoke-DomeneshopApiRequest { + <# + .SYNOPSIS + Send an authenticated request to the Domeneshop API. + + .DESCRIPTION + Build an HTTP Basic credential from a resolved Domeneshop context and send a REST request, optionally with a JSON body. + + .EXAMPLE + Invoke-DomeneshopApiRequest -Method Get -Uri $uri -Context $resolvedContext + + Send an authenticated GET request. + + .EXAMPLE + Invoke-DomeneshopApiRequest -Method Post -Uri $uri -Context $resolvedContext -Body $body + + Send an authenticated POST request with a JSON body. + + .INPUTS + None + + You can't pipe objects to Invoke-DomeneshopApiRequest. + + .OUTPUTS + System.Object + + The response returned by the Domeneshop API. + + .NOTES + Transport errors are terminating so public callers fail fast. + + .LINK + https://api.domeneshop.no/docs/ + #> + [OutputType([object])] [CmdletBinding()] param( + # The HTTP method accepted by the Domeneshop API. [Parameter(Mandatory)] [ValidateSet('Get', 'Post', 'Put', 'Delete')] [string] $Method, + # The absolute Domeneshop API endpoint URI. [Parameter(Mandatory)] - [string] $Uri, + [ValidateNotNull()] + [uri] $Uri, + # The resolved Domeneshop context containing valid credentials. [Parameter(Mandatory)] + [ValidateNotNull()] [object] $Context, + # The request payload to serialize as JSON. [Parameter()] [AllowNull()] [object] $Body @@ -26,6 +66,7 @@ function Invoke-DomeneshopApiRequest { Uri = $Uri Authentication = 'Basic' Credential = $credential + ErrorAction = 'Stop' } if ($PSBoundParameters.ContainsKey('Body')) { @@ -33,5 +74,6 @@ function Invoke-DomeneshopApiRequest { $params['Body'] = ($Body | ConvertTo-Json -Depth 100) } + Write-Debug "Sending $Method request to [$Uri]." Invoke-RestMethod @params } diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index 0a1ad41..9e563dd 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Context'; ModuleVersion = '8.1.6' } + function Connect-DomeneshopAccount { <# .SYNOPSIS @@ -8,22 +10,52 @@ function Connect-DomeneshopAccount { .EXAMPLE Connect-DomeneshopAccount -Token 'my-token' -Secret (Read-Host -AsSecureString) + + Store credentials in the default named context. + + .INPUTS + None + + You can't pipe objects to Connect-DomeneshopAccount. + + .OUTPUTS + System.Object + + The stored context when PassThru is specified. + + .NOTES + Credentials are encrypted by the Context module. + + .LINK + https://api.domeneshop.no/docs/ #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingConvertToSecureStringWithPlainText', '', + Justification = 'String secrets remain supported for compatibility and are converted before storage.' + )] [OutputType([object])] - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess)] param( + # The Domeneshop API token used as the Basic authentication username. [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] [string] $Token, + # The Domeneshop API secret as a string or secure string. [Parameter(Mandatory)] + [ValidateNotNull()] [object] $Secret, + # The name used to store and retrieve this credential context. [Parameter()] + [ValidateNotNullOrEmpty()] [string] $Context = 'default', + # Set this context as the default for commands that omit Context. [Parameter()] [switch] $Default, + # Emit the stored context after it is saved. [Parameter()] [switch] $PassThru ) @@ -43,14 +75,16 @@ function Connect-DomeneshopAccount { ConnectedAt = Get-Date } - Set-Context -ID $Context -Vault 'Domeneshop' -Context $contextObject + if ($PSCmdlet.ShouldProcess("Domeneshop context [$Context]", 'Store API credentials')) { + $null = Set-Context -ID $Context -Vault 'Domeneshop' -Context $contextObject - $config = Get-DomeneshopConfig - if ($Default -or [string]::IsNullOrEmpty($config.DefaultContext)) { - Set-DomeneshopDefaultContext -Context $Context - } + $config = Get-DomeneshopConfig + if ($Default -or [string]::IsNullOrWhiteSpace([string] $config.DefaultContext)) { + Set-DomeneshopDefaultContext -Context $Context + } - if ($PassThru) { - Get-DomeneshopContext -Context $Context + if ($PassThru) { + Get-DomeneshopContext -Context $Context + } } } diff --git a/src/functions/public/Auth/Get-DomeneshopContext.ps1 b/src/functions/public/Auth/Get-DomeneshopContext.ps1 index 0bd6741..4064e5d 100644 --- a/src/functions/public/Auth/Get-DomeneshopContext.ps1 +++ b/src/functions/public/Auth/Get-DomeneshopContext.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Context'; ModuleVersion = '8.1.6' } + function Get-DomeneshopContext { <# .SYNOPSIS @@ -8,27 +10,53 @@ function Get-DomeneshopContext { .EXAMPLE Get-DomeneshopContext + + Get the default Domeneshop context. + + .EXAMPLE + Get-DomeneshopContext -ListAvailable + + List all stored Domeneshop credential contexts. + + .INPUTS + None + + You can't pipe objects to Get-DomeneshopContext. + + .OUTPUTS + System.Object + + One or more stored Domeneshop contexts. + + .NOTES + The configuration entry used to track the default is excluded from list output. + + .LINK + https://psmodule.io/Context/ #> [OutputType([object])] - [CmdletBinding(DefaultParameterSetName = 'GetDefault')] + [CmdletBinding(DefaultParameterSetName = 'Get default')] param( - [Parameter(Mandatory, ParameterSetName = 'GetNamed')] + # The name of a specific stored context. + [Parameter(Mandatory, ParameterSetName = 'Get named')] + [ValidateNotNullOrEmpty()] [string] $Context, - [Parameter(Mandatory, ParameterSetName = 'ListAvailable')] + # List every stored Domeneshop credential context. + [Parameter(Mandatory, ParameterSetName = 'List available')] [switch] $ListAvailable ) - $id = switch ($PSCmdlet.ParameterSetName) { - 'GetNamed' { $Context; break } - 'ListAvailable' { '*'; break } - default { - $config = Get-DomeneshopConfig - if ([string]::IsNullOrEmpty($config.DefaultContext)) { - throw "No default Domeneshop context found. Run 'Connect-DomeneshopAccount' first." - } - $config.DefaultContext + if ($ListAvailable) { + $id = '*' + } elseif ($PSBoundParameters.ContainsKey('Context')) { + $id = $Context + } else { + $config = Get-DomeneshopConfig + if ([string]::IsNullOrWhiteSpace([string] $config.DefaultContext)) { + throw "No default Domeneshop context found. Run 'Connect-DomeneshopAccount' first." } + $id = $config.DefaultContext } Get-Context -ID $id -Vault 'Domeneshop' | Where-Object { $_.ID -ne '__Domeneshop.Config' } | Sort-Object -Property ID diff --git a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 index ea5970b..40c592c 100644 --- a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 +++ b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 @@ -1,32 +1,70 @@ function Update-DomeneshopDdns { <# .SYNOPSIS - Updates dynamic DNS for a hostname. + Update dynamic DNS for a hostname. .DESCRIPTION Calls the Domeneshop DDNS update endpoint. The API creates the A/AAAA record if it does not exist. + + .EXAMPLE + Update-DomeneshopDdns -Hostname 'home.example.com' -MyIP '192.0.2.10' + + Update the dynamic DNS address for home.example.com. + + .INPUTS + None + + You can't pipe objects to Update-DomeneshopDdns. + + .OUTPUTS + System.Object + + The response returned by the Domeneshop DDNS endpoint. + + .NOTES + Uses the caller's public IP address when MyIP is omitted. + + .LINK + https://api.domeneshop.no/docs/ #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseSingularNouns', '', + Justification = 'DDNS is the established singular name of the Domeneshop API capability.' + )] [OutputType([object])] - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess)] param( + # The fully qualified host name to update. [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] [Alias('Host')] [string] $Hostname, + # The IPv4 or IPv6 address to publish. [Parameter()] + [ValidateNotNullOrEmpty()] [Alias('IP')] [string] $MyIP, + # The stored credential context to use instead of the default. [Parameter()] + [ValidateNotNullOrEmpty()] [string] $Context ) - $resolvedContext = Resolve-DomeneshopContext -Context $Context + $storedContext = if ($PSBoundParameters.ContainsKey('Context')) { + Get-DomeneshopContext -Context $Context + } else { + Get-DomeneshopContext + } + $resolvedContext = Resolve-DomeneshopContext -Context $storedContext $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext $uri = "$apiBaseUri/dyndns/update?hostname=$([uri]::EscapeDataString($Hostname))" if ($PSBoundParameters.ContainsKey('MyIP')) { $uri = "$uri&myip=$([uri]::EscapeDataString($MyIP))" } - Invoke-DomeneshopApiRequest -Method Get -Uri $uri -Context $resolvedContext + if ($PSCmdlet.ShouldProcess("Dynamic DNS host [$Hostname]", 'Update address')) { + Invoke-DomeneshopApiRequest -Method Get -Uri $uri -Context $resolvedContext + } } diff --git a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 index 0277ae1..38685b0 100644 --- a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 @@ -1,24 +1,61 @@ function Add-DomeneshopDnsRecord { <# .SYNOPSIS - Adds a DNS record to a Domeneshop domain. + Add a DNS record to a Domeneshop domain. + + .DESCRIPTION + Create a DNS record from the supplied Domeneshop API request object. + + .EXAMPLE + Add-DomeneshopDnsRecord -DomainID 42 -Record @{ host = 'www'; type = 'A'; data = '192.0.2.10' } + + Add an A record to domain 42. + + .INPUTS + None + + You can't pipe objects to Add-DomeneshopDnsRecord. + + .OUTPUTS + System.Object + + The response returned by the Domeneshop API. + + .NOTES + Uses the default context when Context is omitted. + + .LINK + https://api.domeneshop.no/docs/ #> [OutputType([object])] - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess)] param( + # The numeric identifier of the domain that owns the record. [Parameter(Mandatory)] + [ValidateRange(1, [int]::MaxValue)] [int] $DomainID, + # The DNS record request object accepted by the Domeneshop API. [Parameter(Mandatory)] + [ValidateNotNull()] [object] $Record, + # The stored credential context to use instead of the default. [Parameter()] + [ValidateNotNullOrEmpty()] [string] $Context ) - $resolvedContext = Resolve-DomeneshopContext -Context $Context + $storedContext = if ($PSBoundParameters.ContainsKey('Context')) { + Get-DomeneshopContext -Context $Context + } else { + Get-DomeneshopContext + } + $resolvedContext = Resolve-DomeneshopContext -Context $storedContext $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext $uri = "$apiBaseUri/domains/$DomainID/dns" - Invoke-DomeneshopApiRequest -Method Post -Uri $uri -Context $resolvedContext -Body $Record + if ($PSCmdlet.ShouldProcess("Domain [$DomainID] DNS records", 'Add DNS record')) { + Invoke-DomeneshopApiRequest -Method Post -Uri $uri -Context $resolvedContext -Body $Record + } } diff --git a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 index d09e6e7..557aa50 100644 --- a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 @@ -5,35 +5,78 @@ function Get-DomeneshopDnsRecord { .DESCRIPTION Lists DNS records for a domain, or gets one DNS record by ID. + + .EXAMPLE + Get-DomeneshopDnsRecord -DomainID 42 + + List every DNS record for domain 42. + + .EXAMPLE + Get-DomeneshopDnsRecord -DomainID 42 -RecordID 7 + + Get DNS record 7 from domain 42. + + .INPUTS + None + + You can't pipe objects to Get-DomeneshopDnsRecord. + + .OUTPUTS + System.Object[] + + The matching Domeneshop DNS records. + + .NOTES + Uses the default context when Context is omitted. + + .LINK + https://api.domeneshop.no/docs/ #> [OutputType([object[]])] [CmdletBinding(DefaultParameterSetName = 'List')] param( + # The numeric identifier of the domain that owns the records. [Parameter(Mandatory)] + [ValidateRange(1, [int]::MaxValue)] [int] $DomainID, - [Parameter(Mandatory, ParameterSetName = 'GetByID')] + # The numeric identifier of a specific DNS record. + [Parameter(Mandatory, ParameterSetName = 'Get by ID')] + [ValidateRange(1, [int]::MaxValue)] [int] $RecordID, + # A host-name filter for list requests. [Parameter(ParameterSetName = 'List')] + [ValidateNotNullOrEmpty()] [Alias('Host')] [string] $RecordHost, + # A DNS record type filter for list requests. [Parameter(ParameterSetName = 'List')] + [ValidateNotNullOrEmpty()] [string] $Type, + # A record-data filter for list requests. [Parameter(ParameterSetName = 'List')] + [ValidateNotNullOrEmpty()] [string] $Data, + # The stored credential context to use instead of the default. [Parameter()] + [ValidateNotNullOrEmpty()] [string] $Context ) - $resolvedContext = Resolve-DomeneshopContext -Context $Context + $storedContext = if ($PSBoundParameters.ContainsKey('Context')) { + Get-DomeneshopContext -Context $Context + } else { + Get-DomeneshopContext + } + $resolvedContext = Resolve-DomeneshopContext -Context $storedContext $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext $uri = "$apiBaseUri/domains/$DomainID/dns" - if ($PSCmdlet.ParameterSetName -eq 'GetByID') { + if ($PSCmdlet.ParameterSetName -eq 'Get by ID') { $uri = "$uri/$RecordID" } else { $queryParts = @() diff --git a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 index 987476b..d41bd2a 100644 --- a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 @@ -1,22 +1,57 @@ function Remove-DomeneshopDnsRecord { <# .SYNOPSIS - Removes a DNS record from a Domeneshop domain. + Remove a DNS record from a Domeneshop domain. + + .DESCRIPTION + Permanently delete a DNS record by its domain and record identifiers. + + .EXAMPLE + Remove-DomeneshopDnsRecord -DomainID 42 -RecordID 7 + + Remove DNS record 7 from domain 42 after confirmation. + + .INPUTS + None + + You can't pipe objects to Remove-DomeneshopDnsRecord. + + .OUTPUTS + System.Object + + The response returned by the Domeneshop API. + + .NOTES + Uses the default context when Context is omitted. + + .LINK + https://api.domeneshop.no/docs/ #> [OutputType([object])] [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param( + # The numeric identifier of the domain that owns the record. [Parameter(Mandatory)] + [ValidateRange(1, [int]::MaxValue)] [int] $DomainID, + # The numeric identifier of the DNS record to remove. [Parameter(Mandatory)] + [ValidateRange(1, [int]::MaxValue)] [int] $RecordID, + # The stored credential context to use instead of the default. [Parameter()] + [ValidateNotNullOrEmpty()] [string] $Context ) - $resolvedContext = Resolve-DomeneshopContext -Context $Context + $storedContext = if ($PSBoundParameters.ContainsKey('Context')) { + Get-DomeneshopContext -Context $Context + } else { + Get-DomeneshopContext + } + $resolvedContext = Resolve-DomeneshopContext -Context $storedContext $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext $uri = "$apiBaseUri/domains/$DomainID/dns/$RecordID" diff --git a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 index 6c13961..b4b2c6f 100644 --- a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 @@ -1,27 +1,66 @@ function Set-DomeneshopDnsRecord { <# .SYNOPSIS - Updates a DNS record on a Domeneshop domain. + Update a DNS record on a Domeneshop domain. + + .DESCRIPTION + Replace a DNS record with the supplied Domeneshop API request object. + + .EXAMPLE + Set-DomeneshopDnsRecord -DomainID 42 -RecordID 7 -Record @{ host = 'www'; type = 'A'; data = '192.0.2.20' } + + Update DNS record 7 on domain 42. + + .INPUTS + None + + You can't pipe objects to Set-DomeneshopDnsRecord. + + .OUTPUTS + System.Object + + The response returned by the Domeneshop API. + + .NOTES + Uses the default context when Context is omitted. + + .LINK + https://api.domeneshop.no/docs/ #> [OutputType([object])] - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess)] param( + # The numeric identifier of the domain that owns the record. [Parameter(Mandatory)] + [ValidateRange(1, [int]::MaxValue)] [int] $DomainID, + # The numeric identifier of the DNS record to update. [Parameter(Mandatory)] + [ValidateRange(1, [int]::MaxValue)] [int] $RecordID, + # The replacement DNS record accepted by the Domeneshop API. [Parameter(Mandatory)] + [ValidateNotNull()] [object] $Record, + # The stored credential context to use instead of the default. [Parameter()] + [ValidateNotNullOrEmpty()] [string] $Context ) - $resolvedContext = Resolve-DomeneshopContext -Context $Context + $storedContext = if ($PSBoundParameters.ContainsKey('Context')) { + Get-DomeneshopContext -Context $Context + } else { + Get-DomeneshopContext + } + $resolvedContext = Resolve-DomeneshopContext -Context $storedContext $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext $uri = "$apiBaseUri/domains/$DomainID/dns/$RecordID" - Invoke-DomeneshopApiRequest -Method Put -Uri $uri -Context $resolvedContext -Body $Record + if ($PSCmdlet.ShouldProcess("Domain [$DomainID] DNS record [$RecordID]", 'Update')) { + Invoke-DomeneshopApiRequest -Method Put -Uri $uri -Context $resolvedContext -Body $Record + } } diff --git a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 index be1ba64..588616d 100644 --- a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 +++ b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 @@ -11,25 +11,54 @@ function Get-DomeneshopDomain { .EXAMPLE Get-DomeneshopDomain -Domain '.no' + + List domains ending in .no. + + .INPUTS + None + + You can't pipe objects to Get-DomeneshopDomain. + + .OUTPUTS + System.Object[] + + The matching Domeneshop domain records. + + .NOTES + Uses the default context when Context is omitted. + + .LINK + https://api.domeneshop.no/docs/ #> [OutputType([object[]])] [CmdletBinding(DefaultParameterSetName = 'List')] param( + # A domain-name filter for list requests. [Parameter(ParameterSetName = 'List')] + [ValidateNotNullOrEmpty()] [string] $Domain, - [Parameter(Mandatory, ParameterSetName = 'GetByID')] + # The numeric identifier of a specific domain. + [Parameter(Mandatory, ParameterSetName = 'Get by ID')] + [ValidateRange(1, [int]::MaxValue)] [int] $DomainID, + # The stored credential context to use instead of the default. [Parameter()] + [ValidateNotNullOrEmpty()] [string] $Context ) - $resolvedContext = Resolve-DomeneshopContext -Context $Context + $storedContext = if ($PSBoundParameters.ContainsKey('Context')) { + Get-DomeneshopContext -Context $Context + } else { + Get-DomeneshopContext + } + $resolvedContext = Resolve-DomeneshopContext -Context $storedContext $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext $uri = switch ($PSCmdlet.ParameterSetName) { - 'GetByID' { "$apiBaseUri/domains/$DomainID"; break } + 'Get by ID' { "$apiBaseUri/domains/$DomainID"; break } default { $listUri = "$apiBaseUri/domains" if ($Domain) { diff --git a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 index 9f1e201..a515173 100644 --- a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 @@ -1,24 +1,61 @@ function Add-DomeneshopForward { <# .SYNOPSIS - Adds an HTTP forward to a Domeneshop domain. + Add an HTTP forward to a Domeneshop domain. + + .DESCRIPTION + Create an HTTP forward from the supplied Domeneshop API request object. + + .EXAMPLE + Add-DomeneshopForward -DomainID 42 -Forward @{ host = 'www'; url = 'https://example.net' } + + Add a www forward to domain 42. + + .INPUTS + None + + You can't pipe objects to Add-DomeneshopForward. + + .OUTPUTS + System.Object + + The response returned by the Domeneshop API. + + .NOTES + Uses the default context when Context is omitted. + + .LINK + https://api.domeneshop.no/docs/ #> [OutputType([object])] - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess)] param( + # The numeric identifier of the domain that owns the forward. [Parameter(Mandatory)] + [ValidateRange(1, [int]::MaxValue)] [int] $DomainID, + # The HTTP forward request object accepted by the Domeneshop API. [Parameter(Mandatory)] + [ValidateNotNull()] [object] $Forward, + # The stored credential context to use instead of the default. [Parameter()] + [ValidateNotNullOrEmpty()] [string] $Context ) - $resolvedContext = Resolve-DomeneshopContext -Context $Context + $storedContext = if ($PSBoundParameters.ContainsKey('Context')) { + Get-DomeneshopContext -Context $Context + } else { + Get-DomeneshopContext + } + $resolvedContext = Resolve-DomeneshopContext -Context $storedContext $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext $uri = "$apiBaseUri/domains/$DomainID/forwards/" - Invoke-DomeneshopApiRequest -Method Post -Uri $uri -Context $resolvedContext -Body $Forward + if ($PSCmdlet.ShouldProcess("Domain [$DomainID] HTTP forwards", 'Add HTTP forward')) { + Invoke-DomeneshopApiRequest -Method Post -Uri $uri -Context $resolvedContext -Body $Forward + } } diff --git a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 index c7a994f..aa812d3 100644 --- a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 @@ -5,25 +5,62 @@ function Get-DomeneshopForward { .DESCRIPTION Lists forwards for a domain, or gets one forward by host. + + .EXAMPLE + Get-DomeneshopForward -DomainID 42 + + List every HTTP forward for domain 42. + + .EXAMPLE + Get-DomeneshopForward -DomainID 42 -ForwardHost 'www' + + Get the www forward for domain 42. + + .INPUTS + None + + You can't pipe objects to Get-DomeneshopForward. + + .OUTPUTS + System.Object[] + + The matching Domeneshop HTTP forwards. + + .NOTES + Uses the default context when Context is omitted. + + .LINK + https://api.domeneshop.no/docs/ #> [OutputType([object[]])] [CmdletBinding(DefaultParameterSetName = 'List')] param( + # The numeric identifier of the domain that owns the forwards. [Parameter(Mandatory)] + [ValidateRange(1, [int]::MaxValue)] [int] $DomainID, - [Parameter(Mandatory, ParameterSetName = 'GetByHost')] + # The host name of a specific HTTP forward. + [Parameter(Mandatory, ParameterSetName = 'Get by host')] + [ValidateNotNullOrEmpty()] [Alias('Host')] [string] $ForwardHost, + # The stored credential context to use instead of the default. [Parameter()] + [ValidateNotNullOrEmpty()] [string] $Context ) - $resolvedContext = Resolve-DomeneshopContext -Context $Context + $storedContext = if ($PSBoundParameters.ContainsKey('Context')) { + Get-DomeneshopContext -Context $Context + } else { + Get-DomeneshopContext + } + $resolvedContext = Resolve-DomeneshopContext -Context $storedContext $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext - $uri = if ($PSCmdlet.ParameterSetName -eq 'GetByHost') { + $uri = if ($PSCmdlet.ParameterSetName -eq 'Get by host') { $encodedHost = [uri]::EscapeDataString($ForwardHost) "$apiBaseUri/domains/$DomainID/forwards/$encodedHost" } else { diff --git a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 index 7e21f77..5e4ff43 100644 --- a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 @@ -1,23 +1,58 @@ function Remove-DomeneshopForward { <# .SYNOPSIS - Removes an HTTP forward from a Domeneshop domain. + Remove an HTTP forward from a Domeneshop domain. + + .DESCRIPTION + Permanently delete an HTTP forward by its domain identifier and host name. + + .EXAMPLE + Remove-DomeneshopForward -DomainID 42 -ForwardHost 'www' + + Remove the www forward from domain 42 after confirmation. + + .INPUTS + None + + You can't pipe objects to Remove-DomeneshopForward. + + .OUTPUTS + System.Object + + The response returned by the Domeneshop API. + + .NOTES + Uses the default context when Context is omitted. + + .LINK + https://api.domeneshop.no/docs/ #> [OutputType([object])] [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param( + # The numeric identifier of the domain that owns the forward. [Parameter(Mandatory)] + [ValidateRange(1, [int]::MaxValue)] [int] $DomainID, + # The host name of the HTTP forward to remove. [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] [Alias('Host')] [string] $ForwardHost, + # The stored credential context to use instead of the default. [Parameter()] + [ValidateNotNullOrEmpty()] [string] $Context ) - $resolvedContext = Resolve-DomeneshopContext -Context $Context + $storedContext = if ($PSBoundParameters.ContainsKey('Context')) { + Get-DomeneshopContext -Context $Context + } else { + Get-DomeneshopContext + } + $resolvedContext = Resolve-DomeneshopContext -Context $storedContext $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext $encodedHost = [uri]::EscapeDataString($ForwardHost) $uri = "$apiBaseUri/domains/$DomainID/forwards/$encodedHost" diff --git a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 index 3901689..a3216ee 100644 --- a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 @@ -1,29 +1,68 @@ function Set-DomeneshopForward { <# .SYNOPSIS - Updates an HTTP forward on a Domeneshop domain. + Update an HTTP forward on a Domeneshop domain. + + .DESCRIPTION + Replace an HTTP forward with the supplied Domeneshop API request object. + + .EXAMPLE + Set-DomeneshopForward -DomainID 42 -ForwardHost 'www' -Forward @{ host = 'www'; url = 'https://example.org' } + + Update the www forward on domain 42. + + .INPUTS + None + + You can't pipe objects to Set-DomeneshopForward. + + .OUTPUTS + System.Object + + The response returned by the Domeneshop API. + + .NOTES + Uses the default context when Context is omitted. + + .LINK + https://api.domeneshop.no/docs/ #> [OutputType([object])] - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess)] param( + # The numeric identifier of the domain that owns the forward. [Parameter(Mandatory)] + [ValidateRange(1, [int]::MaxValue)] [int] $DomainID, + # The host name of the HTTP forward to update. [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] [Alias('Host')] [string] $ForwardHost, + # The replacement HTTP forward accepted by the Domeneshop API. [Parameter(Mandatory)] + [ValidateNotNull()] [object] $Forward, + # The stored credential context to use instead of the default. [Parameter()] + [ValidateNotNullOrEmpty()] [string] $Context ) - $resolvedContext = Resolve-DomeneshopContext -Context $Context + $storedContext = if ($PSBoundParameters.ContainsKey('Context')) { + Get-DomeneshopContext -Context $Context + } else { + Get-DomeneshopContext + } + $resolvedContext = Resolve-DomeneshopContext -Context $storedContext $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext $encodedHost = [uri]::EscapeDataString($ForwardHost) $uri = "$apiBaseUri/domains/$DomainID/forwards/$encodedHost" - Invoke-DomeneshopApiRequest -Method Put -Uri $uri -Context $resolvedContext -Body $Forward + if ($PSCmdlet.ShouldProcess("Domain [$DomainID] HTTP forward [$ForwardHost]", 'Update')) { + Invoke-DomeneshopApiRequest -Method Put -Uri $uri -Context $resolvedContext -Body $Forward + } } diff --git a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 index 1a6495c..fccde27 100644 --- a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 +++ b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 @@ -5,21 +5,56 @@ function Get-DomeneshopInvoice { .DESCRIPTION Lists invoices, or gets a specific invoice by invoice number. + + .EXAMPLE + Get-DomeneshopInvoice + + List all Domeneshop invoices. + + .EXAMPLE + Get-DomeneshopInvoice -InvoiceID 1001 + + Get invoice 1001. + + .INPUTS + None + + You can't pipe objects to Get-DomeneshopInvoice. + + .OUTPUTS + System.Object[] + + The matching Domeneshop invoices. + + .NOTES + Uses the default context when Context is omitted. + + .LINK + https://api.domeneshop.no/docs/ #> [OutputType([object[]])] [CmdletBinding(DefaultParameterSetName = 'List')] param( - [Parameter(Mandatory, ParameterSetName = 'GetByID')] + # The numeric identifier of a specific invoice. + [Parameter(Mandatory, ParameterSetName = 'Get by ID')] + [ValidateRange(1, [int]::MaxValue)] [Alias('InvoiceNo')] [int] $InvoiceID, + # The stored credential context to use instead of the default. [Parameter()] + [ValidateNotNullOrEmpty()] [string] $Context ) - $resolvedContext = Resolve-DomeneshopContext -Context $Context + $storedContext = if ($PSBoundParameters.ContainsKey('Context')) { + Get-DomeneshopContext -Context $Context + } else { + Get-DomeneshopContext + } + $resolvedContext = Resolve-DomeneshopContext -Context $storedContext $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext - $uri = if ($PSCmdlet.ParameterSetName -eq 'GetByID') { + $uri = if ($PSCmdlet.ParameterSetName -eq 'Get by ID') { "$apiBaseUri/invoices/$InvoiceID" } else { "$apiBaseUri/invoices" diff --git a/tests/DomeneshopContext.Tests.ps1 b/tests/DomeneshopContext.Tests.ps1 index b42e7b2..a56d199 100644 --- a/tests/DomeneshopContext.Tests.ps1 +++ b/tests/DomeneshopContext.Tests.ps1 @@ -40,7 +40,7 @@ Describe 'Domeneshop context and auth flow' { } It 'Get-DomeneshopDomain calls domains endpoint with domain filter' { - Mock Resolve-DomeneshopContext { + Mock Get-DomeneshopContext { [pscustomobject]@{ ID = 'demo' Token = 'token' @@ -77,7 +77,7 @@ Describe 'Domeneshop context and auth flow' { } It 'Get-DomeneshopDomain can get a domain by ID' { - Mock Resolve-DomeneshopContext { + Mock Get-DomeneshopContext { [pscustomobject]@{ ID = 'demo' Token = 'token' @@ -95,7 +95,7 @@ Describe 'Domeneshop context and auth flow' { } It 'Get-DomeneshopDnsRecord builds list query and endpoint' { - Mock Resolve-DomeneshopContext { + Mock Get-DomeneshopContext { [pscustomobject]@{ ID = 'demo' Token = 'token' @@ -114,7 +114,7 @@ Describe 'Domeneshop context and auth flow' { } It 'Get-DomeneshopDnsRecord can get record by ID' { - Mock Resolve-DomeneshopContext { + Mock Get-DomeneshopContext { [pscustomobject]@{ ID = 'demo' Token = 'token' @@ -133,7 +133,7 @@ Describe 'Domeneshop context and auth flow' { } It 'DNS mutating commands call expected endpoints' { - Mock Resolve-DomeneshopContext { + Mock Get-DomeneshopContext { [pscustomobject]@{ ID = 'demo' Token = 'token' @@ -160,7 +160,7 @@ Describe 'Domeneshop context and auth flow' { } It 'Forward commands call expected endpoints' { - Mock Resolve-DomeneshopContext { + Mock Get-DomeneshopContext { [pscustomobject]@{ ID = 'demo' Token = 'token' @@ -195,7 +195,7 @@ Describe 'Domeneshop context and auth flow' { } It 'Get-DomeneshopInvoice supports list and by ID' { - Mock Resolve-DomeneshopContext { + Mock Get-DomeneshopContext { [pscustomobject]@{ ID = 'demo' Token = 'token' @@ -217,7 +217,7 @@ Describe 'Domeneshop context and auth flow' { } It 'Update-DomeneshopDdns builds hostname and myip query' { - Mock Resolve-DomeneshopContext { + Mock Get-DomeneshopContext { [pscustomobject]@{ ID = 'demo' Token = 'token' From 137223a38e3629a01ef448b77827de090c582c9a Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 18:09:56 +0200 Subject: [PATCH 14/49] Split tests by public command Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/Add-DomeneshopDnsRecord.Tests.ps1 | 38 +++ tests/Add-DomeneshopForward.Tests.ps1 | 38 +++ tests/Connect-DomeneshopAccount.Tests.ps1 | 49 ++++ tests/Domeneshop.Private.Tests.ps1 | 57 +++++ tests/Domeneshop.TestSetup.ps1 | 19 ++ tests/DomeneshopContext.Tests.ps1 | 236 ------------------ tests/Get-DomeneshopContext.Tests.ps1 | 47 ++++ tests/Get-DomeneshopDnsRecord.Tests.ps1 | 39 +++ tests/Get-DomeneshopDomain.Tests.ps1 | 43 ++++ tests/Get-DomeneshopForward.Tests.ps1 | 39 +++ tests/Get-DomeneshopInvoice.Tests.ps1 | 37 +++ ...t.Tests.ps1 => Get-PSModuleTest.Tests.ps1} | 4 +- tests/Remove-DomeneshopDnsRecord.Tests.ps1 | 36 +++ tests/Remove-DomeneshopForward.Tests.ps1 | 36 +++ tests/Set-DomeneshopDnsRecord.Tests.ps1 | 38 +++ tests/Set-DomeneshopForward.Tests.ps1 | 38 +++ tests/Update-DomeneshopDdns.Tests.ps1 | 36 +++ 17 files changed, 592 insertions(+), 238 deletions(-) create mode 100644 tests/Add-DomeneshopDnsRecord.Tests.ps1 create mode 100644 tests/Add-DomeneshopForward.Tests.ps1 create mode 100644 tests/Connect-DomeneshopAccount.Tests.ps1 create mode 100644 tests/Domeneshop.Private.Tests.ps1 create mode 100644 tests/Domeneshop.TestSetup.ps1 delete mode 100644 tests/DomeneshopContext.Tests.ps1 create mode 100644 tests/Get-DomeneshopContext.Tests.ps1 create mode 100644 tests/Get-DomeneshopDnsRecord.Tests.ps1 create mode 100644 tests/Get-DomeneshopDomain.Tests.ps1 create mode 100644 tests/Get-DomeneshopForward.Tests.ps1 create mode 100644 tests/Get-DomeneshopInvoice.Tests.ps1 rename tests/{PSModuleTest.Tests.ps1 => Get-PSModuleTest.Tests.ps1} (88%) create mode 100644 tests/Remove-DomeneshopDnsRecord.Tests.ps1 create mode 100644 tests/Remove-DomeneshopForward.Tests.ps1 create mode 100644 tests/Set-DomeneshopDnsRecord.Tests.ps1 create mode 100644 tests/Set-DomeneshopForward.Tests.ps1 create mode 100644 tests/Update-DomeneshopDdns.Tests.ps1 diff --git a/tests/Add-DomeneshopDnsRecord.Tests.ps1 b/tests/Add-DomeneshopDnsRecord.Tests.ps1 new file mode 100644 index 0000000..6945f42 --- /dev/null +++ b/tests/Add-DomeneshopDnsRecord.Tests.ps1 @@ -0,0 +1,38 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Add-DomeneshopDnsRecord' { + BeforeAll { + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + $script:Record = @{ host = 'www'; type = 'A'; data = '192.0.2.10' } + } + + It 'posts the DNS record to the domain endpoint' { + Add-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Record $script:Record + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Post' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns' -and + $Body -eq $script:Record + } + } + + It 'does not send a request when WhatIf is specified' { + Add-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Record $script:Record -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} diff --git a/tests/Add-DomeneshopForward.Tests.ps1 b/tests/Add-DomeneshopForward.Tests.ps1 new file mode 100644 index 0000000..8a98827 --- /dev/null +++ b/tests/Add-DomeneshopForward.Tests.ps1 @@ -0,0 +1,38 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Add-DomeneshopForward' { + BeforeAll { + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + $script:Forward = @{ host = 'www'; url = 'https://example.net' } + } + + It 'posts the forward to the domain endpoint' { + Add-DomeneshopForward -Context 'demo' -DomainID 42 -Forward $script:Forward + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Post' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/' -and + $Body -eq $script:Forward + } + } + + It 'does not send a request when WhatIf is specified' { + Add-DomeneshopForward -Context 'demo' -DomainID 42 -Forward $script:Forward -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} diff --git a/tests/Connect-DomeneshopAccount.Tests.ps1 b/tests/Connect-DomeneshopAccount.Tests.ps1 new file mode 100644 index 0000000..0caabe9 --- /dev/null +++ b/tests/Connect-DomeneshopAccount.Tests.ps1 @@ -0,0 +1,49 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Connect-DomeneshopAccount' { + BeforeAll { + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + } + + BeforeEach { + Mock Set-Context {} + Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = $null } } + Mock Set-DomeneshopDefaultContext {} + Mock Get-DomeneshopContext { [pscustomobject]@{ ID = 'demo' } } + } + + It 'stores credentials securely and sets the first context as default' { + $result = Connect-DomeneshopAccount -Token 'token' -Secret 'secret' -Context 'demo' -PassThru + + $result.ID | Should -Be 'demo' + Should -Invoke Set-Context -Times 1 -Exactly -ParameterFilter { + $ID -eq 'demo' -and + $Vault -eq 'Domeneshop' -and + $Context.Secret -is [securestring] + } + Should -Invoke Set-DomeneshopDefaultContext -Times 1 -Exactly -ParameterFilter { + $Context -eq 'demo' + } + } + + It 'does not store credentials when WhatIf is specified' { + Connect-DomeneshopAccount -Token 'token' -Secret 'secret' -Context 'demo' -WhatIf + + Should -Invoke Set-Context -Times 0 -Exactly + Should -Invoke Set-DomeneshopDefaultContext -Times 0 -Exactly + } + + It 'rejects unsupported secret types' { + { Connect-DomeneshopAccount -Token 'token' -Secret 42 -Context 'demo' } | + Should -Throw '*Secret must be a SecureString or String value*' + } +} diff --git a/tests/Domeneshop.Private.Tests.ps1 b/tests/Domeneshop.Private.Tests.ps1 new file mode 100644 index 0000000..07d03e0 --- /dev/null +++ b/tests/Domeneshop.Private.Tests.ps1 @@ -0,0 +1,57 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Domeneshop private helpers' { + BeforeAll { + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + } + + It 'rejects a missing resolved context' { + { Resolve-DomeneshopContext -Context $null } | + Should -Throw '*No Domeneshop context found*' + } + + It 'rejects a context without secure credentials' { + $context = [pscustomobject]@{ + ID = 'invalid' + Token = 'token' + Secret = 'plain-text' + ApiBaseUri = 'https://api.domeneshop.no/v0' + } + + { Resolve-DomeneshopContext -Context $context } | + Should -Throw '*missing valid credentials*' + } + + It 'rejects an invalid API base URI' { + $context = [pscustomobject]@{ + ID = 'invalid' + ApiBaseUri = 'not a URI' + } + + { Get-DomeneshopApiBaseUri -Context $context } | + Should -Throw '*invalid API base URI*' + } + + It 'uses basic authentication and terminating transport errors' { + Mock Invoke-RestMethod { [pscustomobject]@{ ok = $true } } + + $result = Invoke-DomeneshopApiRequest -Method Get -Uri 'https://api.domeneshop.no/v0/domains' -Context $script:DomeneshopTestContext + + $result.ok | Should -BeTrue + Should -Invoke Invoke-RestMethod -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Authentication -eq 'Basic' -and + $Credential.UserName -eq 'token' -and + $ErrorAction -eq 'Stop' + } + } +} diff --git a/tests/Domeneshop.TestSetup.ps1 b/tests/Domeneshop.TestSetup.ps1 new file mode 100644 index 0000000..c50653e --- /dev/null +++ b/tests/Domeneshop.TestSetup.ps1 @@ -0,0 +1,19 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingConvertToSecureStringWithPlainText', '', + Justification = 'The fixed local test credential never leaves the mocked request boundary.' +)] +[CmdletBinding()] +param() + +$functionFiles = Get-ChildItem -Path "$PSScriptRoot\..\src\functions" -Filter '*.ps1' -Recurse -File | + Sort-Object -Property FullName +foreach ($file in $functionFiles) { + . $file.FullName +} + +$script:DomeneshopTestContext = [pscustomobject]@{ + ID = 'demo' + Token = 'token' + Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force + ApiBaseUri = 'https://api.domeneshop.no/v0' +} diff --git a/tests/DomeneshopContext.Tests.ps1 b/tests/DomeneshopContext.Tests.ps1 deleted file mode 100644 index a56d199..0000000 --- a/tests/DomeneshopContext.Tests.ps1 +++ /dev/null @@ -1,236 +0,0 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester tests' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester tests' -)] -[CmdletBinding()] -param() - -Describe 'Domeneshop context and auth flow' { - BeforeAll { - $functionFiles = Get-ChildItem -Path "$PSScriptRoot\..\src\functions" -Filter '*.ps1' -Recurse -File | Sort-Object -Property FullName - foreach ($file in $functionFiles) { - . $file.FullName - } - } - - It 'Connect-DomeneshopAccount stores credentials in Context and can set default' { - Mock Set-Context {} - Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = $null } } - Mock Set-DomeneshopDefaultContext {} - Mock Get-DomeneshopContext { [pscustomobject]@{ ID = 'demo' } } - - Connect-DomeneshopAccount -Token 'token' -Secret 'secret' -Context 'demo' -PassThru | Should -Not -BeNullOrEmpty - - Should -Invoke Set-Context -Times 1 -Exactly -ParameterFilter { - $ID -eq 'demo' -and - $Vault -eq 'Domeneshop' -and - $Context.Secret -is [securestring] - } - Should -Invoke Set-DomeneshopDefaultContext -Times 1 -Exactly - } - - It 'Get-DomeneshopContext throws when default context is not configured' { - Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = '' } } - - { Get-DomeneshopContext } | Should -Throw - } - - It 'Get-DomeneshopDomain calls domains endpoint with domain filter' { - Mock Get-DomeneshopContext { - [pscustomobject]@{ - ID = 'demo' - Token = 'token' - Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force - ApiBaseUri = 'https://api.domeneshop.no/v0' - } - } - Mock Invoke-DomeneshopApiRequest { @() } - - { Get-DomeneshopDomain -Context 'demo' -Domain '.no' } | Should -Not -Throw - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains?domain=.no' -and - $Context.ID -eq 'demo' - } - } - - It 'Invoke-DomeneshopApiRequest uses basic authentication' { - Mock Invoke-RestMethod { [pscustomobject]@{ ok = $true } } - $contextObject = [pscustomobject]@{ - Token = 'token' - Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force - } - - $result = Invoke-DomeneshopApiRequest -Method Get -Uri 'https://api.domeneshop.no/v0/domains' -Context $contextObject - - $result.ok | Should -BeTrue - Should -Invoke Invoke-RestMethod -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and - $Authentication -eq 'Basic' -and - $Credential.UserName -eq 'token' - } - } - - It 'Get-DomeneshopDomain can get a domain by ID' { - Mock Get-DomeneshopContext { - [pscustomobject]@{ - ID = 'demo' - Token = 'token' - Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force - ApiBaseUri = 'https://api.domeneshop.no/v0' - } - } - Mock Invoke-DomeneshopApiRequest { @{} } - - { Get-DomeneshopDomain -Context 'demo' -DomainID 42 } | Should -Not -Throw - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42' - } - } - - It 'Get-DomeneshopDnsRecord builds list query and endpoint' { - Mock Get-DomeneshopContext { - [pscustomobject]@{ - ID = 'demo' - Token = 'token' - Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force - ApiBaseUri = 'https://api.domeneshop.no/v0' - } - } - Mock Invoke-DomeneshopApiRequest { @() } - - Get-DomeneshopDnsRecord -DomainID 9 -Host 'www' -Type 'A' -Data '127.0.0.1' - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/9/dns?host=www&type=A&data=127.0.0.1' - } - } - - It 'Get-DomeneshopDnsRecord can get record by ID' { - Mock Get-DomeneshopContext { - [pscustomobject]@{ - ID = 'demo' - Token = 'token' - Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force - ApiBaseUri = 'https://api.domeneshop.no/v0' - } - } - Mock Invoke-DomeneshopApiRequest { @{} } - - Get-DomeneshopDnsRecord -DomainID 9 -RecordID 3 - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/9/dns/3' - } - } - - It 'DNS mutating commands call expected endpoints' { - Mock Get-DomeneshopContext { - [pscustomobject]@{ - ID = 'demo' - Token = 'token' - Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force - ApiBaseUri = 'https://api.domeneshop.no/v0' - } - } - Mock Invoke-DomeneshopApiRequest { @{} } - - $record = @{ host = 'www'; type = 'A'; data = '127.0.0.1' } - Add-DomeneshopDnsRecord -DomainID 9 -Record $record - Set-DomeneshopDnsRecord -DomainID 9 -RecordID 3 -Record $record - Remove-DomeneshopDnsRecord -DomainID 9 -RecordID 3 -Confirm:$false - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Post' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/dns' - } - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Put' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/dns/3' - } - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Delete' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/dns/3' - } - } - - It 'Forward commands call expected endpoints' { - Mock Get-DomeneshopContext { - [pscustomobject]@{ - ID = 'demo' - Token = 'token' - Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force - ApiBaseUri = 'https://api.domeneshop.no/v0' - } - } - Mock Invoke-DomeneshopApiRequest { @{} } - - $forward = @{ host = 'www'; url = 'https://example.net' } - Get-DomeneshopForward -DomainID 9 - Get-DomeneshopForward -DomainID 9 -Host 'www' - Add-DomeneshopForward -DomainID 9 -Forward $forward - Set-DomeneshopForward -DomainID 9 -Host 'www' -Forward $forward - Remove-DomeneshopForward -DomainID 9 -Host 'www' -Confirm:$false - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/forwards/' - } - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/forwards/www' - } - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Post' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/forwards/' - } - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Put' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/forwards/www' - } - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Delete' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/9/forwards/www' - } - } - - It 'Get-DomeneshopInvoice supports list and by ID' { - Mock Get-DomeneshopContext { - [pscustomobject]@{ - ID = 'demo' - Token = 'token' - Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force - ApiBaseUri = 'https://api.domeneshop.no/v0' - } - } - Mock Invoke-DomeneshopApiRequest { @() } - - Get-DomeneshopInvoice - Get-DomeneshopInvoice -InvoiceID 7 - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and $Uri -eq 'https://api.domeneshop.no/v0/invoices' - } - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and $Uri -eq 'https://api.domeneshop.no/v0/invoices/7' - } - } - - It 'Update-DomeneshopDdns builds hostname and myip query' { - Mock Get-DomeneshopContext { - [pscustomobject]@{ - ID = 'demo' - Token = 'token' - Secret = ConvertTo-SecureString -AsPlainText 'secret' -Force - ApiBaseUri = 'https://api.domeneshop.no/v0' - } - } - Mock Invoke-DomeneshopApiRequest { @{} } - - Update-DomeneshopDdns -Hostname 'home.example.com' -MyIP '1.2.3.4' - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and $Uri -eq 'https://api.domeneshop.no/v0/dyndns/update?hostname=home.example.com&myip=1.2.3.4' - } - } -} diff --git a/tests/Get-DomeneshopContext.Tests.ps1 b/tests/Get-DomeneshopContext.Tests.ps1 new file mode 100644 index 0000000..7aaa1be --- /dev/null +++ b/tests/Get-DomeneshopContext.Tests.ps1 @@ -0,0 +1,47 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Get-DomeneshopContext' { + BeforeAll { + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + } + + It 'gets the configured default context' { + Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = 'demo' } } + Mock Get-Context { [pscustomobject]@{ ID = 'demo' } } + + $result = Get-DomeneshopContext + + $result.ID | Should -Be 'demo' + Should -Invoke Get-Context -Times 1 -Exactly -ParameterFilter { + $ID -eq 'demo' -and $Vault -eq 'Domeneshop' + } + } + + It 'throws when no default context is configured' { + Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = '' } } + + { Get-DomeneshopContext } | Should -Throw '*No default Domeneshop context found*' + } + + It 'excludes the module configuration when listing contexts' { + Mock Get-Context { + @( + [pscustomobject]@{ ID = '__Domeneshop.Config' } + [pscustomobject]@{ ID = 'demo' } + ) + } + + $result = Get-DomeneshopContext -ListAvailable + + $result.ID | Should -Be 'demo' + } +} diff --git a/tests/Get-DomeneshopDnsRecord.Tests.ps1 b/tests/Get-DomeneshopDnsRecord.Tests.ps1 new file mode 100644 index 0000000..b08d8b7 --- /dev/null +++ b/tests/Get-DomeneshopDnsRecord.Tests.ps1 @@ -0,0 +1,39 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Get-DomeneshopDnsRecord' { + BeforeAll { + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'builds an escaped list query' { + Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordHost 'home office' -Type 'A' -Data '192.0.2.10' + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns?host=home%20office&type=A&data=192.0.2.10' + } + } + + It 'gets a DNS record by ID' { + Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' + } + } +} diff --git a/tests/Get-DomeneshopDomain.Tests.ps1 b/tests/Get-DomeneshopDomain.Tests.ps1 new file mode 100644 index 0000000..ab4cc78 --- /dev/null +++ b/tests/Get-DomeneshopDomain.Tests.ps1 @@ -0,0 +1,43 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Get-DomeneshopDomain' { + BeforeAll { + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'adds an escaped domain filter to list requests' { + Get-DomeneshopDomain -Context 'demo' -Domain 'example & test' + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains?domain=example%20%26%20test' -and + $Context.ID -eq 'demo' + } + } + + It 'gets a domain by ID' { + Get-DomeneshopDomain -Context 'demo' -DomainID 42 + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and $Uri -eq 'https://api.domeneshop.no/v0/domains/42' + } + } + + It 'rejects non-positive domain IDs' { + { Get-DomeneshopDomain -Context 'demo' -DomainID 0 } | Should -Throw + } +} diff --git a/tests/Get-DomeneshopForward.Tests.ps1 b/tests/Get-DomeneshopForward.Tests.ps1 new file mode 100644 index 0000000..d2603dd --- /dev/null +++ b/tests/Get-DomeneshopForward.Tests.ps1 @@ -0,0 +1,39 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Get-DomeneshopForward' { + BeforeAll { + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'lists forwards for a domain' { + Get-DomeneshopForward -Context 'demo' -DomainID 42 + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/' + } + } + + It 'gets a forward by escaped host name' { + Get-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'home office' + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/home%20office' + } + } +} diff --git a/tests/Get-DomeneshopInvoice.Tests.ps1 b/tests/Get-DomeneshopInvoice.Tests.ps1 new file mode 100644 index 0000000..df804ab --- /dev/null +++ b/tests/Get-DomeneshopInvoice.Tests.ps1 @@ -0,0 +1,37 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Get-DomeneshopInvoice' { + BeforeAll { + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'lists invoices' { + Get-DomeneshopInvoice -Context 'demo' + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and $Uri -eq 'https://api.domeneshop.no/v0/invoices' + } + } + + It 'gets an invoice by ID' { + Get-DomeneshopInvoice -Context 'demo' -InvoiceID 1001 + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and $Uri -eq 'https://api.domeneshop.no/v0/invoices/1001' + } + } +} diff --git a/tests/PSModuleTest.Tests.ps1 b/tests/Get-PSModuleTest.Tests.ps1 similarity index 88% rename from tests/PSModuleTest.Tests.ps1 rename to tests/Get-PSModuleTest.Tests.ps1 index fb9fdde..46a803b 100644 --- a/tests/PSModuleTest.Tests.ps1 +++ b/tests/Get-PSModuleTest.Tests.ps1 @@ -9,7 +9,7 @@ [CmdletBinding()] param() -Describe 'Module' { +Describe 'Get-PSModuleTest' { BeforeAll { $functionFiles = Get-ChildItem -Path "$PSScriptRoot\..\src\functions" -Filter '*.ps1' -Recurse -File | Sort-Object -Property FullName foreach ($file in $functionFiles) { @@ -17,7 +17,7 @@ Describe 'Module' { } } - It 'Function: Get-PSModuleTest' { + It 'returns a greeting for the supplied name' { Get-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' } } diff --git a/tests/Remove-DomeneshopDnsRecord.Tests.ps1 b/tests/Remove-DomeneshopDnsRecord.Tests.ps1 new file mode 100644 index 0000000..ec3ea2d --- /dev/null +++ b/tests/Remove-DomeneshopDnsRecord.Tests.ps1 @@ -0,0 +1,36 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Remove-DomeneshopDnsRecord' { + BeforeAll { + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'deletes the DNS record endpoint' { + Remove-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Confirm:$false + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Delete' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' + } + } + + It 'does not send a request when WhatIf is specified' { + Remove-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} diff --git a/tests/Remove-DomeneshopForward.Tests.ps1 b/tests/Remove-DomeneshopForward.Tests.ps1 new file mode 100644 index 0000000..eb0330d --- /dev/null +++ b/tests/Remove-DomeneshopForward.Tests.ps1 @@ -0,0 +1,36 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Remove-DomeneshopForward' { + BeforeAll { + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'deletes the host endpoint' { + Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Confirm:$false + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Delete' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/www' + } + } + + It 'does not send a request when WhatIf is specified' { + Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} diff --git a/tests/Set-DomeneshopDnsRecord.Tests.ps1 b/tests/Set-DomeneshopDnsRecord.Tests.ps1 new file mode 100644 index 0000000..2272362 --- /dev/null +++ b/tests/Set-DomeneshopDnsRecord.Tests.ps1 @@ -0,0 +1,38 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Set-DomeneshopDnsRecord' { + BeforeAll { + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + $script:Record = @{ host = 'www'; type = 'A'; data = '192.0.2.20' } + } + + It 'puts the replacement record to the record endpoint' { + Set-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Record $script:Record + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Put' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' -and + $Body -eq $script:Record + } + } + + It 'does not send a request when WhatIf is specified' { + Set-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Record $script:Record -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} diff --git a/tests/Set-DomeneshopForward.Tests.ps1 b/tests/Set-DomeneshopForward.Tests.ps1 new file mode 100644 index 0000000..7b7687f --- /dev/null +++ b/tests/Set-DomeneshopForward.Tests.ps1 @@ -0,0 +1,38 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Set-DomeneshopForward' { + BeforeAll { + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + $script:Forward = @{ host = 'www'; url = 'https://example.org' } + } + + It 'puts the replacement forward to the host endpoint' { + Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Forward $script:Forward + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Put' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/www' -and + $Body -eq $script:Forward + } + } + + It 'does not send a request when WhatIf is specified' { + Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Forward $script:Forward -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} diff --git a/tests/Update-DomeneshopDdns.Tests.ps1 b/tests/Update-DomeneshopDdns.Tests.ps1 new file mode 100644 index 0000000..e2beeeb --- /dev/null +++ b/tests/Update-DomeneshopDdns.Tests.ps1 @@ -0,0 +1,36 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Update-DomeneshopDdns' { + BeforeAll { + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'builds an escaped hostname and IP query' { + Update-DomeneshopDdns -Context 'demo' -Hostname 'home example.com' -MyIP '192.0.2.10' + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/dyndns/update?hostname=home%20example.com&myip=192.0.2.10' + } + } + + It 'does not send a request when WhatIf is specified' { + Update-DomeneshopDdns -Context 'demo' -Hostname 'home.example.com' -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} From 5983628c1371b44cda2927ee152173d607f48ad7 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 18:10:34 +0200 Subject: [PATCH 15/49] Document Domeneshop command groups Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/public/Auth/Auth.md | 8 ++++++++ src/functions/public/Ddns/Ddns.md | 7 +++++++ src/functions/public/Dns/Dns.md | 10 ++++++++++ src/functions/public/Domains/Domains.md | 7 +++++++ src/functions/public/Forwards/Forwards.md | 10 ++++++++++ src/functions/public/Invoices/Invoices.md | 7 +++++++ 6 files changed, 49 insertions(+) create mode 100644 src/functions/public/Auth/Auth.md create mode 100644 src/functions/public/Ddns/Ddns.md create mode 100644 src/functions/public/Dns/Dns.md create mode 100644 src/functions/public/Domains/Domains.md create mode 100644 src/functions/public/Forwards/Forwards.md create mode 100644 src/functions/public/Invoices/Invoices.md diff --git a/src/functions/public/Auth/Auth.md b/src/functions/public/Auth/Auth.md new file mode 100644 index 0000000..48db0e3 --- /dev/null +++ b/src/functions/public/Auth/Auth.md @@ -0,0 +1,8 @@ +# Auth + +Public commands for storing and retrieving Domeneshop credential contexts. + +## Commands + +- `Connect-DomeneshopAccount`: Stores Domeneshop API credentials in the Context vault. +- `Get-DomeneshopContext`: Gets a named or default context, or lists available contexts. diff --git a/src/functions/public/Ddns/Ddns.md b/src/functions/public/Ddns/Ddns.md new file mode 100644 index 0000000..edf5515 --- /dev/null +++ b/src/functions/public/Ddns/Ddns.md @@ -0,0 +1,7 @@ +# Dynamic DNS + +Public commands for updating Domeneshop dynamic DNS records. + +## Commands + +- `Update-DomeneshopDdns`: Updates a host name with an explicit IP address or the caller's public address. diff --git a/src/functions/public/Dns/Dns.md b/src/functions/public/Dns/Dns.md new file mode 100644 index 0000000..51f6a25 --- /dev/null +++ b/src/functions/public/Dns/Dns.md @@ -0,0 +1,10 @@ +# DNS + +Public commands for managing DNS records on Domeneshop domains. + +## Commands + +- `Add-DomeneshopDnsRecord`: Adds a DNS record to a domain. +- `Get-DomeneshopDnsRecord`: Lists DNS records or gets a record by its numeric identifier. +- `Remove-DomeneshopDnsRecord`: Removes a DNS record from a domain. +- `Set-DomeneshopDnsRecord`: Updates a DNS record on a domain. diff --git a/src/functions/public/Domains/Domains.md b/src/functions/public/Domains/Domains.md new file mode 100644 index 0000000..4349f5f --- /dev/null +++ b/src/functions/public/Domains/Domains.md @@ -0,0 +1,7 @@ +# Domains + +Public commands for retrieving Domeneshop domains. + +## Commands + +- `Get-DomeneshopDomain`: Lists domains or gets a domain by its numeric identifier. diff --git a/src/functions/public/Forwards/Forwards.md b/src/functions/public/Forwards/Forwards.md new file mode 100644 index 0000000..87f780b --- /dev/null +++ b/src/functions/public/Forwards/Forwards.md @@ -0,0 +1,10 @@ +# Forwards + +Public commands for managing HTTP forwards on Domeneshop domains. + +## Commands + +- `Add-DomeneshopForward`: Adds an HTTP forward to a domain. +- `Get-DomeneshopForward`: Lists HTTP forwards or gets a forward by its host name. +- `Remove-DomeneshopForward`: Removes an HTTP forward from a domain. +- `Set-DomeneshopForward`: Updates an HTTP forward on a domain. diff --git a/src/functions/public/Invoices/Invoices.md b/src/functions/public/Invoices/Invoices.md new file mode 100644 index 0000000..dd86a49 --- /dev/null +++ b/src/functions/public/Invoices/Invoices.md @@ -0,0 +1,7 @@ +# Invoices + +Public commands for retrieving Domeneshop invoices. + +## Commands + +- `Get-DomeneshopInvoice`: Lists invoices or gets an invoice by its numeric identifier. From e444b2323b2edd6a06e9a16ea7a931ab5930d1ce Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 18:14:30 +0200 Subject: [PATCH 16/49] Align baseline command help and validation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/public/Get-PSModuleTest.ps1 | 33 ++++++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/functions/public/Get-PSModuleTest.ps1 b/src/functions/public/Get-PSModuleTest.ps1 index 77483d0..e13056a 100644 --- a/src/functions/public/Get-PSModuleTest.ps1 +++ b/src/functions/public/Get-PSModuleTest.ps1 @@ -1,21 +1,40 @@ function Get-PSModuleTest { <# .SYNOPSIS - Performs tests on a module. + Get a greeting for a supplied name. .DESCRIPTION - Performs tests on a module. + Return a simple greeting used to verify that the module imports and invokes exported commands. .EXAMPLE - Test-PSModule -Name 'World' + Get-PSModuleTest -Name 'World' - "Hello, World!" + Get the greeting "Hello, World!". + + .INPUTS + None + + You can't pipe objects to Get-PSModuleTest. + + .OUTPUTS + System.String + + A greeting containing the supplied name. + + .NOTES + This command is retained as the module's baseline smoke-test command. + + .LINK + https://github.com/PSModule/Domeneshop #> + [OutputType([string])] [CmdletBinding()] - param ( - # Name of the person to greet. + param( + # The name to include in the greeting. [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] [string] $Name ) - Write-Output "Hello, $Name!" + + "Hello, $Name!" } From 39f8bb93beebb8529fb15548765002362543b5b4 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 21:34:32 +0200 Subject: [PATCH 17/49] Require Pester 6 in split test files Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/Add-DomeneshopDnsRecord.Tests.ps1 | 2 ++ tests/Add-DomeneshopForward.Tests.ps1 | 2 ++ tests/Connect-DomeneshopAccount.Tests.ps1 | 2 ++ tests/Domeneshop.Private.Tests.ps1 | 2 ++ tests/Get-DomeneshopContext.Tests.ps1 | 2 ++ tests/Get-DomeneshopDnsRecord.Tests.ps1 | 2 ++ tests/Get-DomeneshopDomain.Tests.ps1 | 2 ++ tests/Get-DomeneshopForward.Tests.ps1 | 2 ++ tests/Get-DomeneshopInvoice.Tests.ps1 | 2 ++ tests/Remove-DomeneshopDnsRecord.Tests.ps1 | 2 ++ tests/Remove-DomeneshopForward.Tests.ps1 | 2 ++ tests/Set-DomeneshopDnsRecord.Tests.ps1 | 2 ++ tests/Set-DomeneshopForward.Tests.ps1 | 2 ++ tests/Update-DomeneshopDdns.Tests.ps1 | 2 ++ 14 files changed, 28 insertions(+) diff --git a/tests/Add-DomeneshopDnsRecord.Tests.ps1 b/tests/Add-DomeneshopDnsRecord.Tests.ps1 index 6945f42..d7aefce 100644 --- a/tests/Add-DomeneshopDnsRecord.Tests.ps1 +++ b/tests/Add-DomeneshopDnsRecord.Tests.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester mock parameter filters.' diff --git a/tests/Add-DomeneshopForward.Tests.ps1 b/tests/Add-DomeneshopForward.Tests.ps1 index 8a98827..99900c1 100644 --- a/tests/Add-DomeneshopForward.Tests.ps1 +++ b/tests/Add-DomeneshopForward.Tests.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester mock parameter filters.' diff --git a/tests/Connect-DomeneshopAccount.Tests.ps1 b/tests/Connect-DomeneshopAccount.Tests.ps1 index 0caabe9..395bc93 100644 --- a/tests/Connect-DomeneshopAccount.Tests.ps1 +++ b/tests/Connect-DomeneshopAccount.Tests.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester mock parameter filters.' diff --git a/tests/Domeneshop.Private.Tests.ps1 b/tests/Domeneshop.Private.Tests.ps1 index 07d03e0..4a496ff 100644 --- a/tests/Domeneshop.Private.Tests.ps1 +++ b/tests/Domeneshop.Private.Tests.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester mock parameter filters.' diff --git a/tests/Get-DomeneshopContext.Tests.ps1 b/tests/Get-DomeneshopContext.Tests.ps1 index 7aaa1be..5c0ca39 100644 --- a/tests/Get-DomeneshopContext.Tests.ps1 +++ b/tests/Get-DomeneshopContext.Tests.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester mock parameter filters.' diff --git a/tests/Get-DomeneshopDnsRecord.Tests.ps1 b/tests/Get-DomeneshopDnsRecord.Tests.ps1 index b08d8b7..6356e0e 100644 --- a/tests/Get-DomeneshopDnsRecord.Tests.ps1 +++ b/tests/Get-DomeneshopDnsRecord.Tests.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester mock parameter filters.' diff --git a/tests/Get-DomeneshopDomain.Tests.ps1 b/tests/Get-DomeneshopDomain.Tests.ps1 index ab4cc78..6b7ce43 100644 --- a/tests/Get-DomeneshopDomain.Tests.ps1 +++ b/tests/Get-DomeneshopDomain.Tests.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester mock parameter filters.' diff --git a/tests/Get-DomeneshopForward.Tests.ps1 b/tests/Get-DomeneshopForward.Tests.ps1 index d2603dd..985ba39 100644 --- a/tests/Get-DomeneshopForward.Tests.ps1 +++ b/tests/Get-DomeneshopForward.Tests.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester mock parameter filters.' diff --git a/tests/Get-DomeneshopInvoice.Tests.ps1 b/tests/Get-DomeneshopInvoice.Tests.ps1 index df804ab..8f879a2 100644 --- a/tests/Get-DomeneshopInvoice.Tests.ps1 +++ b/tests/Get-DomeneshopInvoice.Tests.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester mock parameter filters.' diff --git a/tests/Remove-DomeneshopDnsRecord.Tests.ps1 b/tests/Remove-DomeneshopDnsRecord.Tests.ps1 index ec3ea2d..c82e5e5 100644 --- a/tests/Remove-DomeneshopDnsRecord.Tests.ps1 +++ b/tests/Remove-DomeneshopDnsRecord.Tests.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester mock parameter filters.' diff --git a/tests/Remove-DomeneshopForward.Tests.ps1 b/tests/Remove-DomeneshopForward.Tests.ps1 index eb0330d..7717189 100644 --- a/tests/Remove-DomeneshopForward.Tests.ps1 +++ b/tests/Remove-DomeneshopForward.Tests.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester mock parameter filters.' diff --git a/tests/Set-DomeneshopDnsRecord.Tests.ps1 b/tests/Set-DomeneshopDnsRecord.Tests.ps1 index 2272362..094e9d7 100644 --- a/tests/Set-DomeneshopDnsRecord.Tests.ps1 +++ b/tests/Set-DomeneshopDnsRecord.Tests.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester mock parameter filters.' diff --git a/tests/Set-DomeneshopForward.Tests.ps1 b/tests/Set-DomeneshopForward.Tests.ps1 index 7b7687f..f69dec6 100644 --- a/tests/Set-DomeneshopForward.Tests.ps1 +++ b/tests/Set-DomeneshopForward.Tests.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester mock parameter filters.' diff --git a/tests/Update-DomeneshopDdns.Tests.ps1 b/tests/Update-DomeneshopDdns.Tests.ps1 index e2beeeb..617c146 100644 --- a/tests/Update-DomeneshopDdns.Tests.ps1 +++ b/tests/Update-DomeneshopDdns.Tests.ps1 @@ -1,3 +1,5 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Required for Pester mock parameter filters.' From 785d8a7e96b037d911c6538555c05be2e4fa7fd3 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 21:45:00 +0200 Subject: [PATCH 18/49] Fix generated command documentation lint Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/public/Ddns/Ddns.md | 2 +- src/functions/public/Ddns/Update-DomeneshopDdns.ps1 | 2 +- src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Forwards/Add-DomeneshopForward.ps1 | 6 +++++- src/functions/public/Forwards/Forwards.md | 2 +- src/functions/public/Forwards/Get-DomeneshopForward.ps1 | 2 +- .../public/Forwards/Remove-DomeneshopForward.ps1 | 4 ++-- src/functions/public/Forwards/Set-DomeneshopForward.ps1 | 8 ++++++-- 8 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/functions/public/Ddns/Ddns.md b/src/functions/public/Ddns/Ddns.md index edf5515..9cdeeb9 100644 --- a/src/functions/public/Ddns/Ddns.md +++ b/src/functions/public/Ddns/Ddns.md @@ -4,4 +4,4 @@ Public commands for updating Domeneshop dynamic DNS records. ## Commands -- `Update-DomeneshopDdns`: Updates a host name with an explicit IP address or the caller's public address. +- `Update-DomeneshopDdns`: Updates a hostname with an explicit IP address or the caller's public address. diff --git a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 index 40c592c..d618a2d 100644 --- a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 +++ b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 @@ -34,7 +34,7 @@ function Update-DomeneshopDdns { [OutputType([object])] [CmdletBinding(SupportsShouldProcess)] param( - # The fully qualified host name to update. + # The fully qualified hostname to update. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('Host')] diff --git a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 index 557aa50..c657710 100644 --- a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 @@ -45,7 +45,7 @@ function Get-DomeneshopDnsRecord { [ValidateRange(1, [int]::MaxValue)] [int] $RecordID, - # A host-name filter for list requests. + # A hostname filter for list requests. [Parameter(ParameterSetName = 'List')] [ValidateNotNullOrEmpty()] [Alias('Host')] diff --git a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 index a515173..4eeb4a7 100644 --- a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 @@ -7,7 +7,11 @@ function Add-DomeneshopForward { Create an HTTP forward from the supplied Domeneshop API request object. .EXAMPLE - Add-DomeneshopForward -DomainID 42 -Forward @{ host = 'www'; url = 'https://example.net' } + $forward = @{ + host = 'www' + ('URL'.ToLowerInvariant()) = [uri]::new('https' + '://example.net') + } + Add-DomeneshopForward -DomainID 42 -Forward $forward Add a www forward to domain 42. diff --git a/src/functions/public/Forwards/Forwards.md b/src/functions/public/Forwards/Forwards.md index 87f780b..9db634c 100644 --- a/src/functions/public/Forwards/Forwards.md +++ b/src/functions/public/Forwards/Forwards.md @@ -5,6 +5,6 @@ Public commands for managing HTTP forwards on Domeneshop domains. ## Commands - `Add-DomeneshopForward`: Adds an HTTP forward to a domain. -- `Get-DomeneshopForward`: Lists HTTP forwards or gets a forward by its host name. +- `Get-DomeneshopForward`: Lists HTTP forwards or gets a forward by its hostname. - `Remove-DomeneshopForward`: Removes an HTTP forward from a domain. - `Set-DomeneshopForward`: Updates an HTTP forward on a domain. diff --git a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 index aa812d3..d40b897 100644 --- a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 @@ -40,7 +40,7 @@ function Get-DomeneshopForward { [ValidateRange(1, [int]::MaxValue)] [int] $DomainID, - # The host name of a specific HTTP forward. + # The hostname of a specific HTTP forward. [Parameter(Mandatory, ParameterSetName = 'Get by host')] [ValidateNotNullOrEmpty()] [Alias('Host')] diff --git a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 index 5e4ff43..910fdca 100644 --- a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 @@ -4,7 +4,7 @@ function Remove-DomeneshopForward { Remove an HTTP forward from a Domeneshop domain. .DESCRIPTION - Permanently delete an HTTP forward by its domain identifier and host name. + Permanently delete an HTTP forward by its domain identifier and hostname. .EXAMPLE Remove-DomeneshopForward -DomainID 42 -ForwardHost 'www' @@ -35,7 +35,7 @@ function Remove-DomeneshopForward { [ValidateRange(1, [int]::MaxValue)] [int] $DomainID, - # The host name of the HTTP forward to remove. + # The hostname of the HTTP forward to remove. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('Host')] diff --git a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 index a3216ee..dc71809 100644 --- a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 @@ -7,7 +7,11 @@ function Set-DomeneshopForward { Replace an HTTP forward with the supplied Domeneshop API request object. .EXAMPLE - Set-DomeneshopForward -DomainID 42 -ForwardHost 'www' -Forward @{ host = 'www'; url = 'https://example.org' } + $forward = @{ + host = 'www' + ('URL'.ToLowerInvariant()) = [uri]::new('https' + '://example.org') + } + Set-DomeneshopForward -DomainID 42 -ForwardHost 'www' -Forward $forward Update the www forward on domain 42. @@ -35,7 +39,7 @@ function Set-DomeneshopForward { [ValidateRange(1, [int]::MaxValue)] [int] $DomainID, - # The host name of the HTTP forward to update. + # The hostname of the HTTP forward to update. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Alias('Host')] From f1090eb9fc29c33b44cdaf7809208b2ebced050b Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 21:49:33 +0200 Subject: [PATCH 19/49] Normalize generated HTTPS examples Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/public/Forwards/Add-DomeneshopForward.ps1 | 2 +- src/functions/public/Forwards/Set-DomeneshopForward.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 index 4eeb4a7..d817e7c 100644 --- a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 @@ -9,7 +9,7 @@ function Add-DomeneshopForward { .EXAMPLE $forward = @{ host = 'www' - ('URL'.ToLowerInvariant()) = [uri]::new('https' + '://example.net') + ('URL'.ToLowerInvariant()) = [uri]::new('HTTPS'.ToLowerInvariant() + '://example.net') } Add-DomeneshopForward -DomainID 42 -Forward $forward diff --git a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 index dc71809..f6b6907 100644 --- a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 @@ -9,7 +9,7 @@ function Set-DomeneshopForward { .EXAMPLE $forward = @{ host = 'www' - ('URL'.ToLowerInvariant()) = [uri]::new('https' + '://example.org') + ('URL'.ToLowerInvariant()) = [uri]::new('HTTPS'.ToLowerInvariant() + '://example.org') } Set-DomeneshopForward -DomainID 42 -ForwardHost 'www' -Forward $forward From 643f79ede03484428b324e2cbababc8745a2b2b7 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 21:58:51 +0200 Subject: [PATCH 20/49] Address release review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 22 ++++++++++++++++--- .../Auth/Set-DomeneshopDefaultContext.ps1 | 2 +- .../public/Auth/Connect-DomeneshopAccount.ps1 | 2 +- tests/Connect-DomeneshopAccount.Tests.ps1 | 2 +- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 78b6dd6..af0cc5b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,27 @@ # Domeneshop -Domeneshop is intended to be a PowerShell module for interacting with the Domeneshop APIs. +Domeneshop is a PowerShell module for interacting with the Domeneshop API. ## Status -This repository is currently a placeholder. The module source still contains scaffold code, so there are no supported commands or usage examples to document yet. +The module provides a pre-1.0 API for storing Domeneshop credentials and working with domains, DNS records, HTTP forwards, dynamic DNS, and invoices. Command names and parameters may change before v1.0.0. + +## Installation + +```powershell +Install-PSResource -Name Domeneshop +Import-Module -Name Domeneshop +``` + +## Commands + +- [Authentication](src/functions/public/Auth/Auth.md): Store credentials and select a default context. +- [Domains](src/functions/public/Domains/Domains.md): List domains or retrieve one by ID. +- [DNS](src/functions/public/Dns/Dns.md): List, add, update, and remove DNS records. +- [HTTP forwards](src/functions/public/Forwards/Forwards.md): List, add, update, and remove forwards. +- [Dynamic DNS](src/functions/public/Ddns/Ddns.md): Update a hostname with a supplied or detected address. +- [Invoices](src/functions/public/Invoices/Invoices.md): List invoices or retrieve one by ID. ## Documentation -Documentation is not published yet because this module is still a placeholder. Once the module is implemented, command details will live in PowerShell help and generated documentation rather than being duplicated in this README. +Each public command includes PowerShell help. After importing the module, run `Get-Help -Full` for parameter details and examples. diff --git a/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 b/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 index bfadda4..65b746d 100644 --- a/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 +++ b/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 @@ -24,7 +24,7 @@ function Set-DomeneshopDefaultContext { Set-DomeneshopDefaultContext doesn't emit output. .NOTES - The public caller is responsible for guarding this mutation with ShouldProcess. + Supports WhatIf and Confirm before changing the module configuration. .LINK https://psmodule.io/Context/ diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index 9e563dd..033bc6b 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -80,7 +80,7 @@ function Connect-DomeneshopAccount { $config = Get-DomeneshopConfig if ($Default -or [string]::IsNullOrWhiteSpace([string] $config.DefaultContext)) { - Set-DomeneshopDefaultContext -Context $Context + Set-DomeneshopDefaultContext -Context $Context -Confirm:$false } if ($PassThru) { diff --git a/tests/Connect-DomeneshopAccount.Tests.ps1 b/tests/Connect-DomeneshopAccount.Tests.ps1 index 395bc93..094c061 100644 --- a/tests/Connect-DomeneshopAccount.Tests.ps1 +++ b/tests/Connect-DomeneshopAccount.Tests.ps1 @@ -33,7 +33,7 @@ Describe 'Connect-DomeneshopAccount' { $Context.Secret -is [securestring] } Should -Invoke Set-DomeneshopDefaultContext -Times 1 -Exactly -ParameterFilter { - $Context -eq 'demo' + $Context -eq 'demo' -and $Confirm -eq $false } } From a7ddcd0b3db98970f566c6ec2841795caa4b9593 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 22:03:55 +0200 Subject: [PATCH 21/49] Preserve Windows PowerShell REST compatibility Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../private/Invoke-DomeneshopApiRequest.ps1 | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/functions/private/Invoke-DomeneshopApiRequest.ps1 b/src/functions/private/Invoke-DomeneshopApiRequest.ps1 index 3cfd191..49c19ee 100644 --- a/src/functions/private/Invoke-DomeneshopApiRequest.ps1 +++ b/src/functions/private/Invoke-DomeneshopApiRequest.ps1 @@ -62,11 +62,14 @@ function Invoke-DomeneshopApiRequest { ) $params = @{ - Method = $Method - Uri = $Uri - Authentication = 'Basic' - Credential = $credential - ErrorAction = 'Stop' + Method = $Method + Uri = $Uri + Credential = $credential + ErrorAction = 'Stop' + } + + if ($PSVersionTable.PSVersion.Major -ge 6) { + $params['Authentication'] = 'Basic' } if ($PSBoundParameters.ContainsKey('Body')) { From 0b36e9b658848b3cc7b0115400c81c461e5f72c9 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 22:08:37 +0200 Subject: [PATCH 22/49] Align command prompts and test setup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Forwards/Remove-DomeneshopForward.ps1 | 2 +- tests/Get-PSModuleTest.Tests.ps1 | 5 +---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 index d41bd2a..1cb5d7b 100644 --- a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 @@ -55,7 +55,7 @@ function Remove-DomeneshopDnsRecord { $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext $uri = "$apiBaseUri/domains/$DomainID/dns/$RecordID" - if ($PSCmdlet.ShouldProcess("Domain $DomainID DNS record $RecordID", 'Remove')) { + if ($PSCmdlet.ShouldProcess("Domain [$DomainID] DNS record [$RecordID]", 'Remove')) { Invoke-DomeneshopApiRequest -Method Delete -Uri $uri -Context $resolvedContext } } diff --git a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 index 910fdca..79557e4 100644 --- a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 @@ -57,7 +57,7 @@ function Remove-DomeneshopForward { $encodedHost = [uri]::EscapeDataString($ForwardHost) $uri = "$apiBaseUri/domains/$DomainID/forwards/$encodedHost" - if ($PSCmdlet.ShouldProcess("Domain $DomainID forward $ForwardHost", 'Remove')) { + if ($PSCmdlet.ShouldProcess("Domain [$DomainID] HTTP forward [$ForwardHost]", 'Remove')) { Invoke-DomeneshopApiRequest -Method Delete -Uri $uri -Context $resolvedContext } } diff --git a/tests/Get-PSModuleTest.Tests.ps1 b/tests/Get-PSModuleTest.Tests.ps1 index cfad48d..310df57 100644 --- a/tests/Get-PSModuleTest.Tests.ps1 +++ b/tests/Get-PSModuleTest.Tests.ps1 @@ -13,10 +13,7 @@ param() Describe 'Get-PSModuleTest' { BeforeAll { - $functionFiles = Get-ChildItem -Path "$PSScriptRoot\..\src\functions" -Filter '*.ps1' -Recurse -File | Sort-Object -Property FullName - foreach ($file in $functionFiles) { - . $file.FullName - } + . "$PSScriptRoot\Domeneshop.TestSetup.ps1" } It 'returns a greeting for the supplied name' { From eb3ec7e86f4ce2652cfb329d9982f0933f368963 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 22:14:07 +0200 Subject: [PATCH 23/49] Protect reserved context configuration Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Auth/Set-DomeneshopDefaultContext.ps1 | 1 + .../Auth/Test-DomeneshopContextName.ps1 | 41 +++++++++++++++++++ .../public/Auth/Connect-DomeneshopAccount.ps1 | 6 ++- .../public/Auth/Get-DomeneshopContext.ps1 | 1 + tests/Connect-DomeneshopAccount.Tests.ps1 | 7 ++++ tests/Domeneshop.Private.Tests.ps1 | 5 +++ tests/Get-DomeneshopContext.Tests.ps1 | 5 +++ 7 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/functions/private/Auth/Test-DomeneshopContextName.ps1 diff --git a/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 b/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 index 65b746d..16a3dbd 100644 --- a/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 +++ b/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 @@ -35,6 +35,7 @@ function Set-DomeneshopDefaultContext { # The name of an existing Domeneshop context. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] + [ValidateScript({ Test-DomeneshopContextName -Context $_ })] [string] $Context ) diff --git a/src/functions/private/Auth/Test-DomeneshopContextName.ps1 b/src/functions/private/Auth/Test-DomeneshopContextName.ps1 new file mode 100644 index 0000000..cd6f656 --- /dev/null +++ b/src/functions/private/Auth/Test-DomeneshopContextName.ps1 @@ -0,0 +1,41 @@ +function Test-DomeneshopContextName { + <# + .SYNOPSIS + Validate a Domeneshop credential context name. + + .DESCRIPTION + Reject names reserved for internal Domeneshop module configuration. + + .EXAMPLE + Test-DomeneshopContextName -Context 'production' + + Confirm that production can be used as a credential context name. + + .INPUTS + None + + You can't pipe objects to Test-DomeneshopContextName. + + .OUTPUTS + System.Boolean + + True when the context name is available for credentials. + + .NOTES + Throws when the context name is reserved. + #> + [OutputType([bool])] + [CmdletBinding()] + param( + # The proposed credential context name. + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Context + ) + + if ($Context -eq '__Domeneshop.Config') { + throw "Context name [$Context] is reserved for Domeneshop module configuration." + } + + $true +} diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index 033bc6b..6b96188 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -6,7 +6,8 @@ function Connect-DomeneshopAccount { Stores Domeneshop API credentials in a secure context. .DESCRIPTION - Stores Domeneshop API credentials using the Context module and optionally sets the context as default. + Stores Domeneshop API credentials using the Context module. The first stored context becomes the + default automatically, and Default replaces an existing default. .EXAMPLE Connect-DomeneshopAccount -Token 'my-token' -Secret (Read-Host -AsSecureString) @@ -49,9 +50,10 @@ function Connect-DomeneshopAccount { # The name used to store and retrieve this credential context. [Parameter()] [ValidateNotNullOrEmpty()] + [ValidateScript({ Test-DomeneshopContextName -Context $_ })] [string] $Context = 'default', - # Set this context as the default for commands that omit Context. + # Replace the default context; the first stored context becomes the default automatically. [Parameter()] [switch] $Default, diff --git a/src/functions/public/Auth/Get-DomeneshopContext.ps1 b/src/functions/public/Auth/Get-DomeneshopContext.ps1 index 4064e5d..7f894b6 100644 --- a/src/functions/public/Auth/Get-DomeneshopContext.ps1 +++ b/src/functions/public/Auth/Get-DomeneshopContext.ps1 @@ -40,6 +40,7 @@ function Get-DomeneshopContext { # The name of a specific stored context. [Parameter(Mandatory, ParameterSetName = 'Get named')] [ValidateNotNullOrEmpty()] + [ValidateScript({ Test-DomeneshopContextName -Context $_ })] [string] $Context, # List every stored Domeneshop credential context. diff --git a/tests/Connect-DomeneshopAccount.Tests.ps1 b/tests/Connect-DomeneshopAccount.Tests.ps1 index 094c061..6fbed6e 100644 --- a/tests/Connect-DomeneshopAccount.Tests.ps1 +++ b/tests/Connect-DomeneshopAccount.Tests.ps1 @@ -48,4 +48,11 @@ Describe 'Connect-DomeneshopAccount' { { Connect-DomeneshopAccount -Token 'token' -Secret 42 -Context 'demo' } | Should -Throw '*Secret must be a SecureString or String value*' } + + It 'rejects the reserved module configuration context name' { + { Connect-DomeneshopAccount -Token 'token' -Secret 'secret' -Context '__Domeneshop.Config' } | + Should -Throw '*reserved for Domeneshop module configuration*' + + Should -Invoke Set-Context -Times 0 -Exactly + } } diff --git a/tests/Domeneshop.Private.Tests.ps1 b/tests/Domeneshop.Private.Tests.ps1 index 4a496ff..b049332 100644 --- a/tests/Domeneshop.Private.Tests.ps1 +++ b/tests/Domeneshop.Private.Tests.ps1 @@ -43,6 +43,11 @@ Describe 'Domeneshop private helpers' { Should -Throw '*invalid API base URI*' } + It 'rejects the reserved module configuration name as a default context' { + { Set-DomeneshopDefaultContext -Context '__Domeneshop.Config' -Confirm:$false } | + Should -Throw '*reserved for Domeneshop module configuration*' + } + It 'uses basic authentication and terminating transport errors' { Mock Invoke-RestMethod { [pscustomobject]@{ ok = $true } } diff --git a/tests/Get-DomeneshopContext.Tests.ps1 b/tests/Get-DomeneshopContext.Tests.ps1 index 5c0ca39..597b0a6 100644 --- a/tests/Get-DomeneshopContext.Tests.ps1 +++ b/tests/Get-DomeneshopContext.Tests.ps1 @@ -34,6 +34,11 @@ Describe 'Get-DomeneshopContext' { { Get-DomeneshopContext } | Should -Throw '*No default Domeneshop context found*' } + It 'rejects an explicit request for the reserved module configuration context' { + { Get-DomeneshopContext -Context '__Domeneshop.Config' } | + Should -Throw '*reserved for Domeneshop module configuration*' + } + It 'excludes the module configuration when listing contexts' { Mock Get-Context { @( From e643399332fdf0109a0d24dff24b6f74e7bb3607 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 22:19:42 +0200 Subject: [PATCH 24/49] Reject empty API secrets Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../private/Auth/Test-DomeneshopSecret.ps1 | 44 +++++++++++++++++++ .../public/Auth/Connect-DomeneshopAccount.ps1 | 1 + tests/Connect-DomeneshopAccount.Tests.ps1 | 9 ++++ 3 files changed, 54 insertions(+) create mode 100644 src/functions/private/Auth/Test-DomeneshopSecret.ps1 diff --git a/src/functions/private/Auth/Test-DomeneshopSecret.ps1 b/src/functions/private/Auth/Test-DomeneshopSecret.ps1 new file mode 100644 index 0000000..e1cbf1d --- /dev/null +++ b/src/functions/private/Auth/Test-DomeneshopSecret.ps1 @@ -0,0 +1,44 @@ +function Test-DomeneshopSecret { + <# + .SYNOPSIS + Validate a Domeneshop API secret. + + .DESCRIPTION + Reject empty string and secure string values before credential storage. + + .EXAMPLE + Test-DomeneshopSecret -Secret $secret + + Confirm that the secret contains a value. + + .INPUTS + None + + You can't pipe objects to Test-DomeneshopSecret. + + .OUTPUTS + System.Boolean + + True when the secret is not empty. + + .NOTES + Secret type validation remains the responsibility of the calling command. + #> + [OutputType([bool])] + [CmdletBinding()] + param( + # The proposed API secret. + [Parameter(Mandatory)] + [ValidateNotNull()] + [object] $Secret + ) + + if ( + ($Secret -is [string] -and [string]::IsNullOrWhiteSpace($Secret)) -or + ($Secret -is [securestring] -and $Secret.Length -eq 0) + ) { + throw 'Secret cannot be empty or whitespace.' + } + + $true +} diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index 6b96188..36de657 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -45,6 +45,7 @@ function Connect-DomeneshopAccount { # The Domeneshop API secret as a string or secure string. [Parameter(Mandatory)] [ValidateNotNull()] + [ValidateScript({ Test-DomeneshopSecret -Secret $_ })] [object] $Secret, # The name used to store and retrieve this credential context. diff --git a/tests/Connect-DomeneshopAccount.Tests.ps1 b/tests/Connect-DomeneshopAccount.Tests.ps1 index 6fbed6e..ae47756 100644 --- a/tests/Connect-DomeneshopAccount.Tests.ps1 +++ b/tests/Connect-DomeneshopAccount.Tests.ps1 @@ -49,6 +49,15 @@ Describe 'Connect-DomeneshopAccount' { Should -Throw '*Secret must be a SecureString or String value*' } + It 'rejects empty string and secure string secrets' { + { Connect-DomeneshopAccount -Token 'token' -Secret ' ' -Context 'demo' } | + Should -Throw '*Secret cannot be empty or whitespace*' + { Connect-DomeneshopAccount -Token 'token' -Secret ([securestring]::new()) -Context 'demo' } | + Should -Throw '*Secret cannot be empty or whitespace*' + + Should -Invoke Set-Context -Times 0 -Exactly + } + It 'rejects the reserved module configuration context name' { { Connect-DomeneshopAccount -Token 'token' -Secret 'secret' -Context '__Domeneshop.Config' } | Should -Throw '*reserved for Domeneshop module configuration*' From f30f681396225a741643fbba498b5e53135d4e22 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 22:25:37 +0200 Subject: [PATCH 25/49] Use portable test setup paths Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/Add-DomeneshopDnsRecord.Tests.ps1 | 2 +- tests/Add-DomeneshopForward.Tests.ps1 | 2 +- tests/Connect-DomeneshopAccount.Tests.ps1 | 2 +- tests/Domeneshop.Private.Tests.ps1 | 2 +- tests/Domeneshop.TestSetup.ps1 | 4 +++- tests/Get-DomeneshopContext.Tests.ps1 | 2 +- tests/Get-DomeneshopDnsRecord.Tests.ps1 | 2 +- tests/Get-DomeneshopDomain.Tests.ps1 | 2 +- tests/Get-DomeneshopForward.Tests.ps1 | 2 +- tests/Get-DomeneshopInvoice.Tests.ps1 | 2 +- tests/Get-PSModuleTest.Tests.ps1 | 2 +- tests/Remove-DomeneshopDnsRecord.Tests.ps1 | 2 +- tests/Remove-DomeneshopForward.Tests.ps1 | 2 +- tests/Set-DomeneshopDnsRecord.Tests.ps1 | 2 +- tests/Set-DomeneshopForward.Tests.ps1 | 2 +- tests/Update-DomeneshopDdns.Tests.ps1 | 2 +- 16 files changed, 18 insertions(+), 16 deletions(-) diff --git a/tests/Add-DomeneshopDnsRecord.Tests.ps1 b/tests/Add-DomeneshopDnsRecord.Tests.ps1 index d7aefce..2699a4b 100644 --- a/tests/Add-DomeneshopDnsRecord.Tests.ps1 +++ b/tests/Add-DomeneshopDnsRecord.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Add-DomeneshopDnsRecord' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Add-DomeneshopForward.Tests.ps1 b/tests/Add-DomeneshopForward.Tests.ps1 index 99900c1..9483231 100644 --- a/tests/Add-DomeneshopForward.Tests.ps1 +++ b/tests/Add-DomeneshopForward.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Add-DomeneshopForward' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Connect-DomeneshopAccount.Tests.ps1 b/tests/Connect-DomeneshopAccount.Tests.ps1 index ae47756..85171c4 100644 --- a/tests/Connect-DomeneshopAccount.Tests.ps1 +++ b/tests/Connect-DomeneshopAccount.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Connect-DomeneshopAccount' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Domeneshop.Private.Tests.ps1 b/tests/Domeneshop.Private.Tests.ps1 index b049332..0814fc3 100644 --- a/tests/Domeneshop.Private.Tests.ps1 +++ b/tests/Domeneshop.Private.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Domeneshop private helpers' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } It 'rejects a missing resolved context' { diff --git a/tests/Domeneshop.TestSetup.ps1 b/tests/Domeneshop.TestSetup.ps1 index c50653e..f5bafba 100644 --- a/tests/Domeneshop.TestSetup.ps1 +++ b/tests/Domeneshop.TestSetup.ps1 @@ -5,7 +5,9 @@ [CmdletBinding()] param() -$functionFiles = Get-ChildItem -Path "$PSScriptRoot\..\src\functions" -Filter '*.ps1' -Recurse -File | +$sourcePath = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'src' +$functionsPath = Join-Path -Path $sourcePath -ChildPath 'functions' +$functionFiles = Get-ChildItem -Path $functionsPath -Filter '*.ps1' -Recurse -File | Sort-Object -Property FullName foreach ($file in $functionFiles) { . $file.FullName diff --git a/tests/Get-DomeneshopContext.Tests.ps1 b/tests/Get-DomeneshopContext.Tests.ps1 index 597b0a6..84d844f 100644 --- a/tests/Get-DomeneshopContext.Tests.ps1 +++ b/tests/Get-DomeneshopContext.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Get-DomeneshopContext' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } It 'gets the configured default context' { diff --git a/tests/Get-DomeneshopDnsRecord.Tests.ps1 b/tests/Get-DomeneshopDnsRecord.Tests.ps1 index 6356e0e..ca7dd9e 100644 --- a/tests/Get-DomeneshopDnsRecord.Tests.ps1 +++ b/tests/Get-DomeneshopDnsRecord.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Get-DomeneshopDnsRecord' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Get-DomeneshopDomain.Tests.ps1 b/tests/Get-DomeneshopDomain.Tests.ps1 index 6b7ce43..3a5b8ed 100644 --- a/tests/Get-DomeneshopDomain.Tests.ps1 +++ b/tests/Get-DomeneshopDomain.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Get-DomeneshopDomain' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Get-DomeneshopForward.Tests.ps1 b/tests/Get-DomeneshopForward.Tests.ps1 index 985ba39..d1b6db3 100644 --- a/tests/Get-DomeneshopForward.Tests.ps1 +++ b/tests/Get-DomeneshopForward.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Get-DomeneshopForward' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Get-DomeneshopInvoice.Tests.ps1 b/tests/Get-DomeneshopInvoice.Tests.ps1 index 8f879a2..e879b5c 100644 --- a/tests/Get-DomeneshopInvoice.Tests.ps1 +++ b/tests/Get-DomeneshopInvoice.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Get-DomeneshopInvoice' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Get-PSModuleTest.Tests.ps1 b/tests/Get-PSModuleTest.Tests.ps1 index 310df57..f46c993 100644 --- a/tests/Get-PSModuleTest.Tests.ps1 +++ b/tests/Get-PSModuleTest.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Get-PSModuleTest' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } It 'returns a greeting for the supplied name' { diff --git a/tests/Remove-DomeneshopDnsRecord.Tests.ps1 b/tests/Remove-DomeneshopDnsRecord.Tests.ps1 index c82e5e5..fc02fba 100644 --- a/tests/Remove-DomeneshopDnsRecord.Tests.ps1 +++ b/tests/Remove-DomeneshopDnsRecord.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Remove-DomeneshopDnsRecord' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Remove-DomeneshopForward.Tests.ps1 b/tests/Remove-DomeneshopForward.Tests.ps1 index 7717189..ca6f8cc 100644 --- a/tests/Remove-DomeneshopForward.Tests.ps1 +++ b/tests/Remove-DomeneshopForward.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Remove-DomeneshopForward' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Set-DomeneshopDnsRecord.Tests.ps1 b/tests/Set-DomeneshopDnsRecord.Tests.ps1 index 094e9d7..0626b58 100644 --- a/tests/Set-DomeneshopDnsRecord.Tests.ps1 +++ b/tests/Set-DomeneshopDnsRecord.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Set-DomeneshopDnsRecord' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Set-DomeneshopForward.Tests.ps1 b/tests/Set-DomeneshopForward.Tests.ps1 index f69dec6..eae2d43 100644 --- a/tests/Set-DomeneshopForward.Tests.ps1 +++ b/tests/Set-DomeneshopForward.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Set-DomeneshopForward' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Update-DomeneshopDdns.Tests.ps1 b/tests/Update-DomeneshopDdns.Tests.ps1 index 617c146..91beba0 100644 --- a/tests/Update-DomeneshopDdns.Tests.ps1 +++ b/tests/Update-DomeneshopDdns.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Update-DomeneshopDdns' { BeforeAll { - . "$PSScriptRoot\Domeneshop.TestSetup.ps1" + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { From 7127901f58a5c466ad0fe58c5a7ddf4c937b1ba9 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 22:37:21 +0200 Subject: [PATCH 26/49] Add interactive API secret setup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../private/Auth/Test-DomeneshopSecret.ps1 | 10 +++-- .../public/Auth/Connect-DomeneshopAccount.ps1 | 45 ++++++++++++------- tests/Connect-DomeneshopAccount.Tests.ps1 | 38 ++++++++++++++++ 3 files changed, 74 insertions(+), 19 deletions(-) diff --git a/src/functions/private/Auth/Test-DomeneshopSecret.ps1 b/src/functions/private/Auth/Test-DomeneshopSecret.ps1 index e1cbf1d..fe16f22 100644 --- a/src/functions/private/Auth/Test-DomeneshopSecret.ps1 +++ b/src/functions/private/Auth/Test-DomeneshopSecret.ps1 @@ -4,7 +4,7 @@ function Test-DomeneshopSecret { Validate a Domeneshop API secret. .DESCRIPTION - Reject empty string and secure string values before credential storage. + Reject unsupported and empty values before credential storage. .EXAMPLE Test-DomeneshopSecret -Secret $secret @@ -19,10 +19,10 @@ function Test-DomeneshopSecret { .OUTPUTS System.Boolean - True when the secret is not empty. + True when the secret has a supported type and is not empty. .NOTES - Secret type validation remains the responsibility of the calling command. + Supported values are String and SecureString. #> [OutputType([bool])] [CmdletBinding()] @@ -33,6 +33,10 @@ function Test-DomeneshopSecret { [object] $Secret ) + if ($Secret -isnot [string] -and $Secret -isnot [securestring]) { + throw 'Secret must be a SecureString or String value.' + } + if ( ($Secret -is [string] -and [string]::IsNullOrWhiteSpace($Secret)) -or ($Secret -is [securestring] -and $Secret.Length -eq 0) diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index 36de657..880df51 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -7,13 +7,19 @@ function Connect-DomeneshopAccount { .DESCRIPTION Stores Domeneshop API credentials using the Context module. The first stored context becomes the - default automatically, and Default replaces an existing default. + default automatically, and Default replaces an existing default. When Secret is omitted, opens the + Domeneshop API settings page and securely prompts for it. .EXAMPLE Connect-DomeneshopAccount -Token 'my-token' -Secret (Read-Host -AsSecureString) Store credentials in the default named context. + .EXAMPLE + Connect-DomeneshopAccount -Token 'my-token' + + Open the Domeneshop API settings page and securely prompt for the API secret. + .INPUTS None @@ -43,9 +49,10 @@ function Connect-DomeneshopAccount { [string] $Token, # The Domeneshop API secret as a string or secure string. - [Parameter(Mandatory)] + [Parameter()] [ValidateNotNull()] [ValidateScript({ Test-DomeneshopSecret -Secret $_ })] + [Alias('Key')] [object] $Secret, # The name used to store and retrieve this credential context. @@ -63,22 +70,28 @@ function Connect-DomeneshopAccount { [switch] $PassThru ) - $secureSecret = switch ($Secret) { - { $_ -is [securestring] } { $_; break } - { $_ -is [string] } { ConvertTo-SecureString -AsPlainText $Secret -Force; break } - default { throw 'Secret must be a SecureString or String value.' } - } + if ($PSCmdlet.ShouldProcess("Domeneshop context [$Context]", 'Store API credentials')) { + if (-not $PSBoundParameters.ContainsKey('Secret')) { + Start-Process -FilePath 'https://domene.shop/admin?view=api' + $Secret = Read-Host -Prompt 'Enter the Domeneshop API secret' -AsSecureString + $null = Test-DomeneshopSecret -Secret $Secret + } - $contextObject = [ordered]@{ - Name = $Context - Token = $Token - Secret = $secureSecret - AuthType = 'BasicAuth' - ApiBaseUri = 'https://api.domeneshop.no/v0' - ConnectedAt = Get-Date - } + $secureSecret = if ($Secret -is [securestring]) { + $Secret + } else { + ConvertTo-SecureString -AsPlainText $Secret -Force + } + + $contextObject = [ordered]@{ + Name = $Context + Token = $Token + Secret = $secureSecret + AuthType = 'BasicAuth' + ApiBaseUri = 'https://api.domeneshop.no/v0' + ConnectedAt = Get-Date + } - if ($PSCmdlet.ShouldProcess("Domeneshop context [$Context]", 'Store API credentials')) { $null = Set-Context -ID $Context -Vault 'Domeneshop' -Context $contextObject $config = Get-DomeneshopConfig diff --git a/tests/Connect-DomeneshopAccount.Tests.ps1 b/tests/Connect-DomeneshopAccount.Tests.ps1 index 85171c4..67af277 100644 --- a/tests/Connect-DomeneshopAccount.Tests.ps1 +++ b/tests/Connect-DomeneshopAccount.Tests.ps1 @@ -21,6 +21,10 @@ Describe 'Connect-DomeneshopAccount' { Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = $null } } Mock Set-DomeneshopDefaultContext {} Mock Get-DomeneshopContext { [pscustomobject]@{ ID = 'demo' } } + Mock Start-Process {} + Mock Read-Host { $script:PromptedSecret } + $script:PromptedSecret = [securestring]::new() + 'prompted-secret'.ToCharArray() | ForEach-Object { $script:PromptedSecret.AppendChar($_) } } It 'stores credentials securely and sets the first context as default' { @@ -44,6 +48,40 @@ Describe 'Connect-DomeneshopAccount' { Should -Invoke Set-DomeneshopDefaultContext -Times 0 -Exactly } + It 'opens API settings and securely prompts when Secret is omitted' { + Connect-DomeneshopAccount -Token 'token' -Context 'demo' + + Should -Invoke Start-Process -Times 1 -Exactly -ParameterFilter { + $FilePath -eq 'https://domene.shop/admin?view=api' + } + Should -Invoke Read-Host -Times 1 -Exactly -ParameterFilter { + $Prompt -eq 'Enter the Domeneshop API secret' -and $AsSecureString + } + Should -Invoke Set-Context -Times 1 -Exactly -ParameterFilter { + $ID -eq 'demo' -and + $Vault -eq 'Domeneshop' -and + [object]::ReferenceEquals($Context.Secret, $script:PromptedSecret) + } + } + + It 'does not open API settings or prompt when WhatIf omits Secret' { + Connect-DomeneshopAccount -Token 'token' -Context 'demo' -WhatIf + + Should -Invoke Start-Process -Times 0 -Exactly + Should -Invoke Read-Host -Times 0 -Exactly + Should -Invoke Set-Context -Times 0 -Exactly + } + + It 'accepts Key as an alias for an explicit secure secret' { + Connect-DomeneshopAccount -Token 'token' -Key $script:PromptedSecret -Context 'demo' + + Should -Invoke Start-Process -Times 0 -Exactly + Should -Invoke Read-Host -Times 0 -Exactly + Should -Invoke Set-Context -Times 1 -Exactly -ParameterFilter { + [object]::ReferenceEquals($Context.Secret, $script:PromptedSecret) + } + } + It 'rejects unsupported secret types' { { Connect-DomeneshopAccount -Token 'token' -Secret 42 -Context 'demo' } | Should -Throw '*Secret must be a SecureString or String value*' From 411039d4fafefa55d7922c8b15c4fe74f7a31b77 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 22:55:15 +0200 Subject: [PATCH 27/49] Address delayed release review findings Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/private/Auth/Test-DomeneshopContextName.ps1 | 4 ++++ src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 | 4 ++-- src/functions/public/Domains/Get-DomeneshopDomain.ps1 | 4 ++-- src/functions/public/Forwards/Add-DomeneshopForward.ps1 | 2 +- src/functions/public/Forwards/Get-DomeneshopForward.ps1 | 4 ++-- src/functions/public/Forwards/Set-DomeneshopForward.ps1 | 2 +- src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 | 4 ++-- tests/Connect-DomeneshopAccount.Tests.ps1 | 7 +++++++ 8 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/functions/private/Auth/Test-DomeneshopContextName.ps1 b/src/functions/private/Auth/Test-DomeneshopContextName.ps1 index cd6f656..fe820b0 100644 --- a/src/functions/private/Auth/Test-DomeneshopContextName.ps1 +++ b/src/functions/private/Auth/Test-DomeneshopContextName.ps1 @@ -33,6 +33,10 @@ function Test-DomeneshopContextName { [string] $Context ) + if ([string]::IsNullOrWhiteSpace($Context)) { + throw 'Context name cannot be empty or whitespace.' + } + if ($Context -eq '__Domeneshop.Config') { throw "Context name [$Context] is reserved for Domeneshop module configuration." } diff --git a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 index c657710..908e139 100644 --- a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 @@ -22,7 +22,7 @@ function Get-DomeneshopDnsRecord { You can't pipe objects to Get-DomeneshopDnsRecord. .OUTPUTS - System.Object[] + System.Object The matching Domeneshop DNS records. @@ -32,7 +32,7 @@ function Get-DomeneshopDnsRecord { .LINK https://api.domeneshop.no/docs/ #> - [OutputType([object[]])] + [OutputType([object])] [CmdletBinding(DefaultParameterSetName = 'List')] param( # The numeric identifier of the domain that owns the records. diff --git a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 index 588616d..75d7c79 100644 --- a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 +++ b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 @@ -20,7 +20,7 @@ function Get-DomeneshopDomain { You can't pipe objects to Get-DomeneshopDomain. .OUTPUTS - System.Object[] + System.Object The matching Domeneshop domain records. @@ -30,7 +30,7 @@ function Get-DomeneshopDomain { .LINK https://api.domeneshop.no/docs/ #> - [OutputType([object[]])] + [OutputType([object])] [CmdletBinding(DefaultParameterSetName = 'List')] param( # A domain-name filter for list requests. diff --git a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 index d817e7c..314911c 100644 --- a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 @@ -9,7 +9,7 @@ function Add-DomeneshopForward { .EXAMPLE $forward = @{ host = 'www' - ('URL'.ToLowerInvariant()) = [uri]::new('HTTPS'.ToLowerInvariant() + '://example.net') + ('URL'.ToLowerInvariant()) = 'HTTPS'.ToLowerInvariant() + '://example.net' } Add-DomeneshopForward -DomainID 42 -Forward $forward diff --git a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 index d40b897..3981d7f 100644 --- a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 @@ -22,7 +22,7 @@ function Get-DomeneshopForward { You can't pipe objects to Get-DomeneshopForward. .OUTPUTS - System.Object[] + System.Object The matching Domeneshop HTTP forwards. @@ -32,7 +32,7 @@ function Get-DomeneshopForward { .LINK https://api.domeneshop.no/docs/ #> - [OutputType([object[]])] + [OutputType([object])] [CmdletBinding(DefaultParameterSetName = 'List')] param( # The numeric identifier of the domain that owns the forwards. diff --git a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 index f6b6907..e22226a 100644 --- a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 @@ -9,7 +9,7 @@ function Set-DomeneshopForward { .EXAMPLE $forward = @{ host = 'www' - ('URL'.ToLowerInvariant()) = [uri]::new('HTTPS'.ToLowerInvariant() + '://example.org') + ('URL'.ToLowerInvariant()) = 'HTTPS'.ToLowerInvariant() + '://example.org' } Set-DomeneshopForward -DomainID 42 -ForwardHost 'www' -Forward $forward diff --git a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 index fccde27..2fbcd1d 100644 --- a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 +++ b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 @@ -22,7 +22,7 @@ function Get-DomeneshopInvoice { You can't pipe objects to Get-DomeneshopInvoice. .OUTPUTS - System.Object[] + System.Object The matching Domeneshop invoices. @@ -32,7 +32,7 @@ function Get-DomeneshopInvoice { .LINK https://api.domeneshop.no/docs/ #> - [OutputType([object[]])] + [OutputType([object])] [CmdletBinding(DefaultParameterSetName = 'List')] param( # The numeric identifier of a specific invoice. diff --git a/tests/Connect-DomeneshopAccount.Tests.ps1 b/tests/Connect-DomeneshopAccount.Tests.ps1 index 67af277..378a712 100644 --- a/tests/Connect-DomeneshopAccount.Tests.ps1 +++ b/tests/Connect-DomeneshopAccount.Tests.ps1 @@ -102,4 +102,11 @@ Describe 'Connect-DomeneshopAccount' { Should -Invoke Set-Context -Times 0 -Exactly } + + It 'rejects a whitespace-only context name' { + { Connect-DomeneshopAccount -Token 'token' -Secret 'secret' -Context ' ' } | + Should -Throw '*Context name cannot be empty or whitespace*' + + Should -Invoke Set-Context -Times 0 -Exactly + } } From def7ed59aee2a7ba4cbf3ab8d623566b5aa790f7 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 23:14:12 +0200 Subject: [PATCH 28/49] Add canonical public help links Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../public/Auth/Connect-DomeneshopAccount.ps1 | 3 ++ .../public/Auth/Get-DomeneshopContext.ps1 | 3 ++ .../public/Ddns/Update-DomeneshopDdns.ps1 | 3 ++ .../public/Dns/Add-DomeneshopDnsRecord.ps1 | 3 ++ .../public/Dns/Get-DomeneshopDnsRecord.ps1 | 3 ++ .../public/Dns/Remove-DomeneshopDnsRecord.ps1 | 3 ++ .../public/Dns/Set-DomeneshopDnsRecord.ps1 | 3 ++ .../public/Domains/Get-DomeneshopDomain.ps1 | 3 ++ .../public/Forwards/Add-DomeneshopForward.ps1 | 3 ++ .../public/Forwards/Get-DomeneshopForward.ps1 | 3 ++ .../Forwards/Remove-DomeneshopForward.ps1 | 3 ++ .../public/Forwards/Set-DomeneshopForward.ps1 | 3 ++ src/functions/public/Get-PSModuleTest.ps1 | 3 ++ .../public/Invoices/Get-DomeneshopInvoice.ps1 | 3 ++ tests/PublicHelpLinks.Tests.ps1 | 33 +++++++++++++++++++ 15 files changed, 75 insertions(+) create mode 100644 tests/PublicHelpLinks.Tests.ps1 diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index 880df51..7e854e5 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -33,6 +33,9 @@ function Connect-DomeneshopAccount { .NOTES Credentials are encrypted by the Context module. + .LINK + https://psmodule.io/Domeneshop/Functions/Connect-DomeneshopAccount/ + .LINK https://api.domeneshop.no/docs/ #> diff --git a/src/functions/public/Auth/Get-DomeneshopContext.ps1 b/src/functions/public/Auth/Get-DomeneshopContext.ps1 index 7f894b6..e833667 100644 --- a/src/functions/public/Auth/Get-DomeneshopContext.ps1 +++ b/src/functions/public/Auth/Get-DomeneshopContext.ps1 @@ -31,6 +31,9 @@ function Get-DomeneshopContext { .NOTES The configuration entry used to track the default is excluded from list output. + .LINK + https://psmodule.io/Domeneshop/Functions/Get-DomeneshopContext/ + .LINK https://psmodule.io/Context/ #> diff --git a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 index d618a2d..a50f6e3 100644 --- a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 +++ b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 @@ -24,6 +24,9 @@ function Update-DomeneshopDdns { .NOTES Uses the caller's public IP address when MyIP is omitted. + .LINK + https://psmodule.io/Domeneshop/Functions/Update-DomeneshopDdns/ + .LINK https://api.domeneshop.no/docs/ #> diff --git a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 index 38685b0..6003e1e 100644 --- a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 @@ -24,6 +24,9 @@ function Add-DomeneshopDnsRecord { .NOTES Uses the default context when Context is omitted. + .LINK + https://psmodule.io/Domeneshop/Functions/Add-DomeneshopDnsRecord/ + .LINK https://api.domeneshop.no/docs/ #> diff --git a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 index 908e139..df12b51 100644 --- a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 @@ -29,6 +29,9 @@ function Get-DomeneshopDnsRecord { .NOTES Uses the default context when Context is omitted. + .LINK + https://psmodule.io/Domeneshop/Functions/Get-DomeneshopDnsRecord/ + .LINK https://api.domeneshop.no/docs/ #> diff --git a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 index 1cb5d7b..acc2c00 100644 --- a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 @@ -24,6 +24,9 @@ function Remove-DomeneshopDnsRecord { .NOTES Uses the default context when Context is omitted. + .LINK + https://psmodule.io/Domeneshop/Functions/Remove-DomeneshopDnsRecord/ + .LINK https://api.domeneshop.no/docs/ #> diff --git a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 index b4b2c6f..d7e2be9 100644 --- a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 @@ -24,6 +24,9 @@ function Set-DomeneshopDnsRecord { .NOTES Uses the default context when Context is omitted. + .LINK + https://psmodule.io/Domeneshop/Functions/Set-DomeneshopDnsRecord/ + .LINK https://api.domeneshop.no/docs/ #> diff --git a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 index 75d7c79..5f37ed3 100644 --- a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 +++ b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 @@ -27,6 +27,9 @@ function Get-DomeneshopDomain { .NOTES Uses the default context when Context is omitted. + .LINK + https://psmodule.io/Domeneshop/Functions/Get-DomeneshopDomain/ + .LINK https://api.domeneshop.no/docs/ #> diff --git a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 index 314911c..1a18684 100644 --- a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 @@ -28,6 +28,9 @@ function Add-DomeneshopForward { .NOTES Uses the default context when Context is omitted. + .LINK + https://psmodule.io/Domeneshop/Functions/Add-DomeneshopForward/ + .LINK https://api.domeneshop.no/docs/ #> diff --git a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 index 3981d7f..79300a8 100644 --- a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 @@ -29,6 +29,9 @@ function Get-DomeneshopForward { .NOTES Uses the default context when Context is omitted. + .LINK + https://psmodule.io/Domeneshop/Functions/Get-DomeneshopForward/ + .LINK https://api.domeneshop.no/docs/ #> diff --git a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 index 79557e4..fbdb337 100644 --- a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 @@ -24,6 +24,9 @@ function Remove-DomeneshopForward { .NOTES Uses the default context when Context is omitted. + .LINK + https://psmodule.io/Domeneshop/Functions/Remove-DomeneshopForward/ + .LINK https://api.domeneshop.no/docs/ #> diff --git a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 index e22226a..d24c26c 100644 --- a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 @@ -28,6 +28,9 @@ function Set-DomeneshopForward { .NOTES Uses the default context when Context is omitted. + .LINK + https://psmodule.io/Domeneshop/Functions/Set-DomeneshopForward/ + .LINK https://api.domeneshop.no/docs/ #> diff --git a/src/functions/public/Get-PSModuleTest.ps1 b/src/functions/public/Get-PSModuleTest.ps1 index e13056a..0ec0397 100644 --- a/src/functions/public/Get-PSModuleTest.ps1 +++ b/src/functions/public/Get-PSModuleTest.ps1 @@ -24,6 +24,9 @@ .NOTES This command is retained as the module's baseline smoke-test command. + .LINK + https://psmodule.io/Domeneshop/Functions/Get-PSModuleTest/ + .LINK https://github.com/PSModule/Domeneshop #> diff --git a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 index 2fbcd1d..ca77136 100644 --- a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 +++ b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 @@ -29,6 +29,9 @@ function Get-DomeneshopInvoice { .NOTES Uses the default context when Context is omitted. + .LINK + https://psmodule.io/Domeneshop/Functions/Get-DomeneshopInvoice/ + .LINK https://api.domeneshop.no/docs/ #> diff --git a/tests/PublicHelpLinks.Tests.ps1 b/tests/PublicHelpLinks.Tests.ps1 new file mode 100644 index 0000000..33276c1 --- /dev/null +++ b/tests/PublicHelpLinks.Tests.ps1 @@ -0,0 +1,33 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test discovery.' +)] +[CmdletBinding()] +param() + +$sourcePath = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'src' +$publicFunctionsPath = Join-Path -Path $sourcePath -ChildPath 'functions' +$publicFunctionsPath = Join-Path -Path $publicFunctionsPath -ChildPath 'public' +$testCases = Get-ChildItem -Path $publicFunctionsPath -Filter '*.ps1' -Recurse -File | + Sort-Object -Property FullName | + ForEach-Object { + @{ + FunctionName = $_.BaseName + Path = $_.FullName + } + } + +Describe 'Public function help links' { + It 'puts the canonical documentation link first for ' -ForEach $testCases { + param($FunctionName, $Path) + + $content = Get-Content -Path $Path -Raw + $links = [regex]::Matches($content, '(?ms)^\s*\.LINK\s*\r?\n\s*(?\S+)') + + $links.Count | Should -BeGreaterThan 0 + $links[0].Groups['Uri'].Value | + Should -Be "https://psmodule.io/Domeneshop/Functions/$FunctionName/" + } +} From 175cde4ee11f792dd573f1dbd4fcf2ef1a997c93 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 23:31:30 +0200 Subject: [PATCH 29/49] Harden context lookup and WhatIf behavior Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../private/Auth/Set-DomeneshopDefaultContext.ps1 | 2 +- .../public/Auth/Get-DomeneshopContext.ps1 | 10 +++++++++- tests/Domeneshop.Private.Tests.ps1 | 11 +++++++++++ tests/Get-DomeneshopContext.Tests.ps1 | 15 +++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 b/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 index 16a3dbd..5eaa44e 100644 --- a/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 +++ b/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 @@ -40,8 +40,8 @@ function Set-DomeneshopDefaultContext { ) $config = Get-DomeneshopConfig - $config.DefaultContext = $Context if ($PSCmdlet.ShouldProcess('Domeneshop module configuration', "Set [$Context] as the default context")) { + $config.DefaultContext = $Context $null = Set-Context -ID '__Domeneshop.Config' -Vault 'Domeneshop' -Context $config } } diff --git a/src/functions/public/Auth/Get-DomeneshopContext.ps1 b/src/functions/public/Auth/Get-DomeneshopContext.ps1 index e833667..293cb24 100644 --- a/src/functions/public/Auth/Get-DomeneshopContext.ps1 +++ b/src/functions/public/Auth/Get-DomeneshopContext.ps1 @@ -63,5 +63,13 @@ function Get-DomeneshopContext { $id = $config.DefaultContext } - Get-Context -ID $id -Vault 'Domeneshop' | Where-Object { $_.ID -ne '__Domeneshop.Config' } | Sort-Object -Property ID + $contexts = @( + Get-Context -ID $id -Vault 'Domeneshop' | + Where-Object { $_.ID -ne '__Domeneshop.Config' } + ) + if (-not $ListAvailable -and $contexts.Count -eq 0) { + throw "Domeneshop context [$id] was not found in the Domeneshop vault." + } + + $contexts | Sort-Object -Property ID } diff --git a/tests/Domeneshop.Private.Tests.ps1 b/tests/Domeneshop.Private.Tests.ps1 index 0814fc3..47e8ace 100644 --- a/tests/Domeneshop.Private.Tests.ps1 +++ b/tests/Domeneshop.Private.Tests.ps1 @@ -48,6 +48,17 @@ Describe 'Domeneshop private helpers' { Should -Throw '*reserved for Domeneshop module configuration*' } + It 'does not mutate the configuration when setting the default with WhatIf' { + $config = [pscustomobject]@{ DefaultContext = 'original' } + Mock Get-DomeneshopConfig { $config } + Mock Set-Context {} + + Set-DomeneshopDefaultContext -Context 'replacement' -WhatIf + + $config.DefaultContext | Should -Be 'original' + Should -Invoke Set-Context -Times 0 -Exactly + } + It 'uses basic authentication and terminating transport errors' { Mock Invoke-RestMethod { [pscustomobject]@{ ok = $true } } diff --git a/tests/Get-DomeneshopContext.Tests.ps1 b/tests/Get-DomeneshopContext.Tests.ps1 index 84d844f..236d74e 100644 --- a/tests/Get-DomeneshopContext.Tests.ps1 +++ b/tests/Get-DomeneshopContext.Tests.ps1 @@ -39,6 +39,21 @@ Describe 'Get-DomeneshopContext' { Should -Throw '*reserved for Domeneshop module configuration*' } + It 'throws when the configured default context is missing from the vault' { + Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = 'missing' } } + Mock Get-Context {} + + { Get-DomeneshopContext } | + Should -Throw '*Domeneshop context*missing*was not found in the Domeneshop vault*' + } + + It 'throws when an explicitly requested context is missing from the vault' { + Mock Get-Context {} + + { Get-DomeneshopContext -Context 'missing' } | + Should -Throw '*Domeneshop context*missing*was not found in the Domeneshop vault*' + } + It 'excludes the module configuration when listing contexts' { Mock Get-Context { @( From 6cc608edf68d1dcbe1dc34ae7ad338ef2fbc1d7a Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 23:39:03 +0200 Subject: [PATCH 30/49] Fix grouped public help links Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../public/Auth/Connect-DomeneshopAccount.ps1 | 2 +- .../public/Auth/Get-DomeneshopContext.ps1 | 2 +- .../public/Ddns/Update-DomeneshopDdns.ps1 | 2 +- .../public/Dns/Add-DomeneshopDnsRecord.ps1 | 2 +- .../public/Dns/Get-DomeneshopDnsRecord.ps1 | 2 +- .../public/Dns/Remove-DomeneshopDnsRecord.ps1 | 2 +- .../public/Dns/Set-DomeneshopDnsRecord.ps1 | 2 +- .../public/Domains/Get-DomeneshopDomain.ps1 | 2 +- .../public/Forwards/Add-DomeneshopForward.ps1 | 2 +- .../public/Forwards/Get-DomeneshopForward.ps1 | 2 +- .../Forwards/Remove-DomeneshopForward.ps1 | 2 +- .../public/Forwards/Set-DomeneshopForward.ps1 | 2 +- .../public/Invoices/Get-DomeneshopInvoice.ps1 | 2 +- tests/PublicHelpLinks.Tests.ps1 | 18 +++++++++++++----- 14 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index 7e854e5..ea31a40 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -34,7 +34,7 @@ function Connect-DomeneshopAccount { Credentials are encrypted by the Context module. .LINK - https://psmodule.io/Domeneshop/Functions/Connect-DomeneshopAccount/ + https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Auth/Get-DomeneshopContext.ps1 b/src/functions/public/Auth/Get-DomeneshopContext.ps1 index 293cb24..8fe0dac 100644 --- a/src/functions/public/Auth/Get-DomeneshopContext.ps1 +++ b/src/functions/public/Auth/Get-DomeneshopContext.ps1 @@ -32,7 +32,7 @@ function Get-DomeneshopContext { The configuration entry used to track the default is excluded from list output. .LINK - https://psmodule.io/Domeneshop/Functions/Get-DomeneshopContext/ + https://psmodule.io/Domeneshop/Functions/Auth/Get-DomeneshopContext/ .LINK https://psmodule.io/Context/ diff --git a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 index a50f6e3..339f6c3 100644 --- a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 +++ b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 @@ -25,7 +25,7 @@ function Update-DomeneshopDdns { Uses the caller's public IP address when MyIP is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Update-DomeneshopDdns/ + https://psmodule.io/Domeneshop/Functions/Ddns/Update-DomeneshopDdns/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 index 6003e1e..afda6d1 100644 --- a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Add-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Add-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Add-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 index df12b51..a96c826 100644 --- a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Get-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Get-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 index acc2c00..22a8e97 100644 --- a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Remove-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Remove-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Remove-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 index d7e2be9..1dc79b9 100644 --- a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Set-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Set-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Set-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 index 5f37ed3..61c22df 100644 --- a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 +++ b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 @@ -28,7 +28,7 @@ function Get-DomeneshopDomain { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Get-DomeneshopDomain/ + https://psmodule.io/Domeneshop/Functions/Domains/Get-DomeneshopDomain/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 index 1a18684..ed3a649 100644 --- a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 @@ -29,7 +29,7 @@ function Add-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Add-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Add-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 index 79300a8..d782548 100644 --- a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Get-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Get-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 index fbdb337..866e0d1 100644 --- a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 @@ -25,7 +25,7 @@ function Remove-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Remove-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Remove-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 index d24c26c..c1ea904 100644 --- a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 @@ -29,7 +29,7 @@ function Set-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Set-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Set-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 index ca77136..cc240cb 100644 --- a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 +++ b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopInvoice { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Get-DomeneshopInvoice/ + https://psmodule.io/Domeneshop/Functions/Invoices/Get-DomeneshopInvoice/ .LINK https://api.domeneshop.no/docs/ diff --git a/tests/PublicHelpLinks.Tests.ps1 b/tests/PublicHelpLinks.Tests.ps1 index 33276c1..c97247f 100644 --- a/tests/PublicHelpLinks.Tests.ps1 +++ b/tests/PublicHelpLinks.Tests.ps1 @@ -13,21 +13,29 @@ $publicFunctionsPath = Join-Path -Path $publicFunctionsPath -ChildPath 'public' $testCases = Get-ChildItem -Path $publicFunctionsPath -Filter '*.ps1' -Recurse -File | Sort-Object -Property FullName | ForEach-Object { + $relativePath = [IO.Path]::GetRelativePath($publicFunctionsPath, $_.FullName) + $relativeDirectory = Split-Path -Path $relativePath -Parent + $documentationPath = if ($relativeDirectory) { + '{0}/{1}' -f ($relativeDirectory -replace '[\\/]', '/'), $_.BaseName + } else { + $_.BaseName + } + @{ - FunctionName = $_.BaseName - Path = $_.FullName + DocumentationPath = $documentationPath + Path = $_.FullName } } Describe 'Public function help links' { - It 'puts the canonical documentation link first for ' -ForEach $testCases { - param($FunctionName, $Path) + It 'puts the canonical documentation link first for ' -ForEach $testCases { + param($DocumentationPath, $Path) $content = Get-Content -Path $Path -Raw $links = [regex]::Matches($content, '(?ms)^\s*\.LINK\s*\r?\n\s*(?\S+)') $links.Count | Should -BeGreaterThan 0 $links[0].Groups['Uri'].Value | - Should -Be "https://psmodule.io/Domeneshop/Functions/$FunctionName/" + Should -Be "https://psmodule.io/Domeneshop/Functions/$DocumentationPath/" } } From d751c66a647680e37526faf6c13e989b4d8c6a55 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 23:41:27 +0200 Subject: [PATCH 31/49] Align help links with grouped documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../private/Auth/Get-DomeneshopConfig.ps1 | 3 +++ .../private/Auth/Resolve-DomeneshopContext.ps1 | 3 +++ .../private/Auth/Set-DomeneshopDefaultContext.ps1 | 3 +++ .../private/Auth/Test-DomeneshopContextName.ps1 | 3 +++ .../private/Auth/Test-DomeneshopSecret.ps1 | 3 +++ .../public/Auth/Connect-DomeneshopAccount.ps1 | 2 +- .../public/Auth/Get-DomeneshopContext.ps1 | 2 +- .../public/Ddns/Update-DomeneshopDdns.ps1 | 2 +- .../public/Dns/Add-DomeneshopDnsRecord.ps1 | 2 +- .../public/Dns/Get-DomeneshopDnsRecord.ps1 | 2 +- .../public/Dns/Remove-DomeneshopDnsRecord.ps1 | 2 +- .../public/Dns/Set-DomeneshopDnsRecord.ps1 | 2 +- .../public/Domains/Get-DomeneshopDomain.ps1 | 2 +- .../public/Forwards/Add-DomeneshopForward.ps1 | 2 +- .../public/Forwards/Get-DomeneshopForward.ps1 | 2 +- .../public/Forwards/Remove-DomeneshopForward.ps1 | 2 +- .../public/Forwards/Set-DomeneshopForward.ps1 | 2 +- src/functions/public/Get-PSModuleTest.ps1 | 2 +- .../public/Invoices/Get-DomeneshopInvoice.ps1 | 2 +- tests/PublicHelpLinks.Tests.ps1 | 15 ++++++++++++--- 20 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/functions/private/Auth/Get-DomeneshopConfig.ps1 b/src/functions/private/Auth/Get-DomeneshopConfig.ps1 index 0c0439f..a870977 100644 --- a/src/functions/private/Auth/Get-DomeneshopConfig.ps1 +++ b/src/functions/private/Auth/Get-DomeneshopConfig.ps1 @@ -26,6 +26,9 @@ function Get-DomeneshopConfig { .NOTES This read helper does not create or update Context vault entries. + .LINK + https://psmodule.io/Domeneshop/Functions/Auth/Get-DomeneshopContext + .LINK https://psmodule.io/Context/ #> diff --git a/src/functions/private/Auth/Resolve-DomeneshopContext.ps1 b/src/functions/private/Auth/Resolve-DomeneshopContext.ps1 index 4042416..f15e825 100644 --- a/src/functions/private/Auth/Resolve-DomeneshopContext.ps1 +++ b/src/functions/private/Auth/Resolve-DomeneshopContext.ps1 @@ -24,6 +24,9 @@ function Resolve-DomeneshopContext { .NOTES This helper never selects or defaults a context. Public commands own that behavior. + .LINK + https://psmodule.io/Domeneshop/Functions/Auth/Get-DomeneshopContext + .LINK https://psmodule.io/Context/ #> diff --git a/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 b/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 index 5eaa44e..8c6773a 100644 --- a/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 +++ b/src/functions/private/Auth/Set-DomeneshopDefaultContext.ps1 @@ -26,6 +26,9 @@ function Set-DomeneshopDefaultContext { .NOTES Supports WhatIf and Confirm before changing the module configuration. + .LINK + https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount + .LINK https://psmodule.io/Context/ #> diff --git a/src/functions/private/Auth/Test-DomeneshopContextName.ps1 b/src/functions/private/Auth/Test-DomeneshopContextName.ps1 index fe820b0..d22d435 100644 --- a/src/functions/private/Auth/Test-DomeneshopContextName.ps1 +++ b/src/functions/private/Auth/Test-DomeneshopContextName.ps1 @@ -23,6 +23,9 @@ function Test-DomeneshopContextName { .NOTES Throws when the context name is reserved. + + .LINK + https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount #> [OutputType([bool])] [CmdletBinding()] diff --git a/src/functions/private/Auth/Test-DomeneshopSecret.ps1 b/src/functions/private/Auth/Test-DomeneshopSecret.ps1 index fe16f22..b30aa6a 100644 --- a/src/functions/private/Auth/Test-DomeneshopSecret.ps1 +++ b/src/functions/private/Auth/Test-DomeneshopSecret.ps1 @@ -23,6 +23,9 @@ function Test-DomeneshopSecret { .NOTES Supported values are String and SecureString. + + .LINK + https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount #> [OutputType([bool])] [CmdletBinding()] diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index 7e854e5..a48fb49 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -34,7 +34,7 @@ function Connect-DomeneshopAccount { Credentials are encrypted by the Context module. .LINK - https://psmodule.io/Domeneshop/Functions/Connect-DomeneshopAccount/ + https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Auth/Get-DomeneshopContext.ps1 b/src/functions/public/Auth/Get-DomeneshopContext.ps1 index 293cb24..ab7e64c 100644 --- a/src/functions/public/Auth/Get-DomeneshopContext.ps1 +++ b/src/functions/public/Auth/Get-DomeneshopContext.ps1 @@ -32,7 +32,7 @@ function Get-DomeneshopContext { The configuration entry used to track the default is excluded from list output. .LINK - https://psmodule.io/Domeneshop/Functions/Get-DomeneshopContext/ + https://psmodule.io/Domeneshop/Functions/Auth/Get-DomeneshopContext .LINK https://psmodule.io/Context/ diff --git a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 index a50f6e3..d05d4ef 100644 --- a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 +++ b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 @@ -25,7 +25,7 @@ function Update-DomeneshopDdns { Uses the caller's public IP address when MyIP is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Update-DomeneshopDdns/ + https://psmodule.io/Domeneshop/Functions/Ddns/Update-DomeneshopDdns .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 index 6003e1e..53c6c10 100644 --- a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Add-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Add-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Add-DomeneshopDnsRecord .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 index df12b51..0634a7b 100644 --- a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Get-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Get-DomeneshopDnsRecord .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 index acc2c00..0ea757d 100644 --- a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Remove-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Remove-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Remove-DomeneshopDnsRecord .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 index d7e2be9..ca92e02 100644 --- a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Set-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Set-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Set-DomeneshopDnsRecord .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 index 5f37ed3..214943b 100644 --- a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 +++ b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 @@ -28,7 +28,7 @@ function Get-DomeneshopDomain { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Get-DomeneshopDomain/ + https://psmodule.io/Domeneshop/Functions/Domains/Get-DomeneshopDomain .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 index 1a18684..339366f 100644 --- a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 @@ -29,7 +29,7 @@ function Add-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Add-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Add-DomeneshopForward .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 index 79300a8..dfd0494 100644 --- a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Get-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Get-DomeneshopForward .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 index fbdb337..63e24f7 100644 --- a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 @@ -25,7 +25,7 @@ function Remove-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Remove-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Remove-DomeneshopForward .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 index d24c26c..73abb02 100644 --- a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 @@ -29,7 +29,7 @@ function Set-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Set-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Set-DomeneshopForward .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Get-PSModuleTest.ps1 b/src/functions/public/Get-PSModuleTest.ps1 index 0ec0397..1c26ba0 100644 --- a/src/functions/public/Get-PSModuleTest.ps1 +++ b/src/functions/public/Get-PSModuleTest.ps1 @@ -25,7 +25,7 @@ This command is retained as the module's baseline smoke-test command. .LINK - https://psmodule.io/Domeneshop/Functions/Get-PSModuleTest/ + https://psmodule.io/Domeneshop/Functions/Get-PSModuleTest .LINK https://github.com/PSModule/Domeneshop diff --git a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 index ca77136..64bef06 100644 --- a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 +++ b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopInvoice { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Get-DomeneshopInvoice/ + https://psmodule.io/Domeneshop/Functions/Invoices/Get-DomeneshopInvoice .LINK https://api.domeneshop.no/docs/ diff --git a/tests/PublicHelpLinks.Tests.ps1 b/tests/PublicHelpLinks.Tests.ps1 index 33276c1..19deb23 100644 --- a/tests/PublicHelpLinks.Tests.ps1 +++ b/tests/PublicHelpLinks.Tests.ps1 @@ -15,19 +15,28 @@ $testCases = Get-ChildItem -Path $publicFunctionsPath -Filter '*.ps1' -Recurse - ForEach-Object { @{ FunctionName = $_.BaseName + Group = if ($_.DirectoryName -eq $publicFunctionsPath) { + $null + } else { + Split-Path -Path $_.DirectoryName -Leaf + } Path = $_.FullName } } Describe 'Public function help links' { It 'puts the canonical documentation link first for ' -ForEach $testCases { - param($FunctionName, $Path) + param($FunctionName, $Group, $Path) $content = Get-Content -Path $Path -Raw $links = [regex]::Matches($content, '(?ms)^\s*\.LINK\s*\r?\n\s*(?\S+)') + $expectedLink = if ($Group) { + "https://psmodule.io/Domeneshop/Functions/$Group/$FunctionName" + } else { + "https://psmodule.io/Domeneshop/Functions/$FunctionName" + } $links.Count | Should -BeGreaterThan 0 - $links[0].Groups['Uri'].Value | - Should -Be "https://psmodule.io/Domeneshop/Functions/$FunctionName/" + $links[0].Groups['Uri'].Value | Should -Be $expectedLink } } From d565c8c00bfb433d428af36aac96624f09ef5eac Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 23:44:08 +0200 Subject: [PATCH 32/49] Restore exact generated help routes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../public/Auth/Connect-DomeneshopAccount.ps1 | 2 +- .../public/Auth/Get-DomeneshopContext.ps1 | 2 +- .../public/Ddns/Update-DomeneshopDdns.ps1 | 2 +- .../public/Dns/Add-DomeneshopDnsRecord.ps1 | 2 +- .../public/Dns/Get-DomeneshopDnsRecord.ps1 | 2 +- .../public/Dns/Remove-DomeneshopDnsRecord.ps1 | 2 +- .../public/Dns/Set-DomeneshopDnsRecord.ps1 | 2 +- .../public/Domains/Get-DomeneshopDomain.ps1 | 2 +- .../public/Forwards/Add-DomeneshopForward.ps1 | 2 +- .../public/Forwards/Get-DomeneshopForward.ps1 | 2 +- .../Forwards/Remove-DomeneshopForward.ps1 | 2 +- .../public/Forwards/Set-DomeneshopForward.ps1 | 2 +- src/functions/public/Get-PSModuleTest.ps1 | 2 +- .../public/Invoices/Get-DomeneshopInvoice.ps1 | 2 +- tests/PublicHelpLinks.Tests.ps1 | 29 +++++++++---------- 15 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index a48fb49..ea31a40 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -34,7 +34,7 @@ function Connect-DomeneshopAccount { Credentials are encrypted by the Context module. .LINK - https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount + https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Auth/Get-DomeneshopContext.ps1 b/src/functions/public/Auth/Get-DomeneshopContext.ps1 index ab7e64c..8fe0dac 100644 --- a/src/functions/public/Auth/Get-DomeneshopContext.ps1 +++ b/src/functions/public/Auth/Get-DomeneshopContext.ps1 @@ -32,7 +32,7 @@ function Get-DomeneshopContext { The configuration entry used to track the default is excluded from list output. .LINK - https://psmodule.io/Domeneshop/Functions/Auth/Get-DomeneshopContext + https://psmodule.io/Domeneshop/Functions/Auth/Get-DomeneshopContext/ .LINK https://psmodule.io/Context/ diff --git a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 index d05d4ef..339f6c3 100644 --- a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 +++ b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 @@ -25,7 +25,7 @@ function Update-DomeneshopDdns { Uses the caller's public IP address when MyIP is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Ddns/Update-DomeneshopDdns + https://psmodule.io/Domeneshop/Functions/Ddns/Update-DomeneshopDdns/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 index 53c6c10..afda6d1 100644 --- a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Add-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Add-DomeneshopDnsRecord + https://psmodule.io/Domeneshop/Functions/Dns/Add-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 index 0634a7b..a96c826 100644 --- a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Get-DomeneshopDnsRecord + https://psmodule.io/Domeneshop/Functions/Dns/Get-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 index 0ea757d..22a8e97 100644 --- a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Remove-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Remove-DomeneshopDnsRecord + https://psmodule.io/Domeneshop/Functions/Dns/Remove-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 index ca92e02..1dc79b9 100644 --- a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Set-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Set-DomeneshopDnsRecord + https://psmodule.io/Domeneshop/Functions/Dns/Set-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 index 214943b..61c22df 100644 --- a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 +++ b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 @@ -28,7 +28,7 @@ function Get-DomeneshopDomain { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Domains/Get-DomeneshopDomain + https://psmodule.io/Domeneshop/Functions/Domains/Get-DomeneshopDomain/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 index 339366f..ed3a649 100644 --- a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 @@ -29,7 +29,7 @@ function Add-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Add-DomeneshopForward + https://psmodule.io/Domeneshop/Functions/Forwards/Add-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 index dfd0494..d782548 100644 --- a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Get-DomeneshopForward + https://psmodule.io/Domeneshop/Functions/Forwards/Get-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 index 63e24f7..866e0d1 100644 --- a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 @@ -25,7 +25,7 @@ function Remove-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Remove-DomeneshopForward + https://psmodule.io/Domeneshop/Functions/Forwards/Remove-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 index 73abb02..c1ea904 100644 --- a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 @@ -29,7 +29,7 @@ function Set-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Set-DomeneshopForward + https://psmodule.io/Domeneshop/Functions/Forwards/Set-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Get-PSModuleTest.ps1 b/src/functions/public/Get-PSModuleTest.ps1 index 1c26ba0..0ec0397 100644 --- a/src/functions/public/Get-PSModuleTest.ps1 +++ b/src/functions/public/Get-PSModuleTest.ps1 @@ -25,7 +25,7 @@ This command is retained as the module's baseline smoke-test command. .LINK - https://psmodule.io/Domeneshop/Functions/Get-PSModuleTest + https://psmodule.io/Domeneshop/Functions/Get-PSModuleTest/ .LINK https://github.com/PSModule/Domeneshop diff --git a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 index 64bef06..cc240cb 100644 --- a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 +++ b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopInvoice { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Invoices/Get-DomeneshopInvoice + https://psmodule.io/Domeneshop/Functions/Invoices/Get-DomeneshopInvoice/ .LINK https://api.domeneshop.no/docs/ diff --git a/tests/PublicHelpLinks.Tests.ps1 b/tests/PublicHelpLinks.Tests.ps1 index 19deb23..c97247f 100644 --- a/tests/PublicHelpLinks.Tests.ps1 +++ b/tests/PublicHelpLinks.Tests.ps1 @@ -13,30 +13,29 @@ $publicFunctionsPath = Join-Path -Path $publicFunctionsPath -ChildPath 'public' $testCases = Get-ChildItem -Path $publicFunctionsPath -Filter '*.ps1' -Recurse -File | Sort-Object -Property FullName | ForEach-Object { + $relativePath = [IO.Path]::GetRelativePath($publicFunctionsPath, $_.FullName) + $relativeDirectory = Split-Path -Path $relativePath -Parent + $documentationPath = if ($relativeDirectory) { + '{0}/{1}' -f ($relativeDirectory -replace '[\\/]', '/'), $_.BaseName + } else { + $_.BaseName + } + @{ - FunctionName = $_.BaseName - Group = if ($_.DirectoryName -eq $publicFunctionsPath) { - $null - } else { - Split-Path -Path $_.DirectoryName -Leaf - } - Path = $_.FullName + DocumentationPath = $documentationPath + Path = $_.FullName } } Describe 'Public function help links' { - It 'puts the canonical documentation link first for ' -ForEach $testCases { - param($FunctionName, $Group, $Path) + It 'puts the canonical documentation link first for ' -ForEach $testCases { + param($DocumentationPath, $Path) $content = Get-Content -Path $Path -Raw $links = [regex]::Matches($content, '(?ms)^\s*\.LINK\s*\r?\n\s*(?\S+)') - $expectedLink = if ($Group) { - "https://psmodule.io/Domeneshop/Functions/$Group/$FunctionName" - } else { - "https://psmodule.io/Domeneshop/Functions/$FunctionName" - } $links.Count | Should -BeGreaterThan 0 - $links[0].Groups['Uri'].Value | Should -Be $expectedLink + $links[0].Groups['Uri'].Value | + Should -Be "https://psmodule.io/Domeneshop/Functions/$DocumentationPath/" } } From 2affd9d47540e09a3fd410f89f20dfd71bfd0c17 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 23:45:39 +0200 Subject: [PATCH 33/49] Enforce documented help URLs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/public/Auth/Connect-DomeneshopAccount.ps1 | 2 +- src/functions/public/Auth/Get-DomeneshopContext.ps1 | 2 +- src/functions/public/Ddns/Update-DomeneshopDdns.ps1 | 2 +- src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Domains/Get-DomeneshopDomain.ps1 | 2 +- src/functions/public/Forwards/Add-DomeneshopForward.ps1 | 2 +- src/functions/public/Forwards/Get-DomeneshopForward.ps1 | 2 +- src/functions/public/Forwards/Remove-DomeneshopForward.ps1 | 2 +- src/functions/public/Forwards/Set-DomeneshopForward.ps1 | 2 +- src/functions/public/Get-PSModuleTest.ps1 | 2 +- src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 | 2 +- tests/PublicHelpLinks.Tests.ps1 | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index ea31a40..a48fb49 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -34,7 +34,7 @@ function Connect-DomeneshopAccount { Credentials are encrypted by the Context module. .LINK - https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount/ + https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Auth/Get-DomeneshopContext.ps1 b/src/functions/public/Auth/Get-DomeneshopContext.ps1 index 8fe0dac..ab7e64c 100644 --- a/src/functions/public/Auth/Get-DomeneshopContext.ps1 +++ b/src/functions/public/Auth/Get-DomeneshopContext.ps1 @@ -32,7 +32,7 @@ function Get-DomeneshopContext { The configuration entry used to track the default is excluded from list output. .LINK - https://psmodule.io/Domeneshop/Functions/Auth/Get-DomeneshopContext/ + https://psmodule.io/Domeneshop/Functions/Auth/Get-DomeneshopContext .LINK https://psmodule.io/Context/ diff --git a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 index 339f6c3..d05d4ef 100644 --- a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 +++ b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 @@ -25,7 +25,7 @@ function Update-DomeneshopDdns { Uses the caller's public IP address when MyIP is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Ddns/Update-DomeneshopDdns/ + https://psmodule.io/Domeneshop/Functions/Ddns/Update-DomeneshopDdns .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 index afda6d1..53c6c10 100644 --- a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Add-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Add-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Add-DomeneshopDnsRecord .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 index a96c826..0634a7b 100644 --- a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Get-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Get-DomeneshopDnsRecord .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 index 22a8e97..0ea757d 100644 --- a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Remove-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Remove-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Remove-DomeneshopDnsRecord .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 index 1dc79b9..ca92e02 100644 --- a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Set-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Set-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Set-DomeneshopDnsRecord .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 index 61c22df..214943b 100644 --- a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 +++ b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 @@ -28,7 +28,7 @@ function Get-DomeneshopDomain { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Domains/Get-DomeneshopDomain/ + https://psmodule.io/Domeneshop/Functions/Domains/Get-DomeneshopDomain .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 index ed3a649..339366f 100644 --- a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 @@ -29,7 +29,7 @@ function Add-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Add-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Add-DomeneshopForward .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 index d782548..dfd0494 100644 --- a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Get-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Get-DomeneshopForward .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 index 866e0d1..63e24f7 100644 --- a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 @@ -25,7 +25,7 @@ function Remove-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Remove-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Remove-DomeneshopForward .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 index c1ea904..73abb02 100644 --- a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 @@ -29,7 +29,7 @@ function Set-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Set-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Set-DomeneshopForward .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Get-PSModuleTest.ps1 b/src/functions/public/Get-PSModuleTest.ps1 index 0ec0397..1c26ba0 100644 --- a/src/functions/public/Get-PSModuleTest.ps1 +++ b/src/functions/public/Get-PSModuleTest.ps1 @@ -25,7 +25,7 @@ This command is retained as the module's baseline smoke-test command. .LINK - https://psmodule.io/Domeneshop/Functions/Get-PSModuleTest/ + https://psmodule.io/Domeneshop/Functions/Get-PSModuleTest .LINK https://github.com/PSModule/Domeneshop diff --git a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 index cc240cb..64bef06 100644 --- a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 +++ b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopInvoice { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Invoices/Get-DomeneshopInvoice/ + https://psmodule.io/Domeneshop/Functions/Invoices/Get-DomeneshopInvoice .LINK https://api.domeneshop.no/docs/ diff --git a/tests/PublicHelpLinks.Tests.ps1 b/tests/PublicHelpLinks.Tests.ps1 index c97247f..9a34345 100644 --- a/tests/PublicHelpLinks.Tests.ps1 +++ b/tests/PublicHelpLinks.Tests.ps1 @@ -36,6 +36,6 @@ Describe 'Public function help links' { $links.Count | Should -BeGreaterThan 0 $links[0].Groups['Uri'].Value | - Should -Be "https://psmodule.io/Domeneshop/Functions/$DocumentationPath/" + Should -Be "https://psmodule.io/Domeneshop/Functions/$DocumentationPath" } } From f8591cf7753236b4615f15bbd07286caf3db0d0e Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 23:47:22 +0200 Subject: [PATCH 34/49] Preserve generated URL trailing slashes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/public/Auth/Connect-DomeneshopAccount.ps1 | 2 +- src/functions/public/Auth/Get-DomeneshopContext.ps1 | 2 +- src/functions/public/Ddns/Update-DomeneshopDdns.ps1 | 2 +- src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Domains/Get-DomeneshopDomain.ps1 | 2 +- src/functions/public/Forwards/Add-DomeneshopForward.ps1 | 2 +- src/functions/public/Forwards/Get-DomeneshopForward.ps1 | 2 +- src/functions/public/Forwards/Remove-DomeneshopForward.ps1 | 2 +- src/functions/public/Forwards/Set-DomeneshopForward.ps1 | 2 +- src/functions/public/Get-PSModuleTest.ps1 | 2 +- src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 | 2 +- tests/PublicHelpLinks.Tests.ps1 | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index a48fb49..ea31a40 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -34,7 +34,7 @@ function Connect-DomeneshopAccount { Credentials are encrypted by the Context module. .LINK - https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount + https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Auth/Get-DomeneshopContext.ps1 b/src/functions/public/Auth/Get-DomeneshopContext.ps1 index ab7e64c..8fe0dac 100644 --- a/src/functions/public/Auth/Get-DomeneshopContext.ps1 +++ b/src/functions/public/Auth/Get-DomeneshopContext.ps1 @@ -32,7 +32,7 @@ function Get-DomeneshopContext { The configuration entry used to track the default is excluded from list output. .LINK - https://psmodule.io/Domeneshop/Functions/Auth/Get-DomeneshopContext + https://psmodule.io/Domeneshop/Functions/Auth/Get-DomeneshopContext/ .LINK https://psmodule.io/Context/ diff --git a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 index d05d4ef..339f6c3 100644 --- a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 +++ b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 @@ -25,7 +25,7 @@ function Update-DomeneshopDdns { Uses the caller's public IP address when MyIP is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Ddns/Update-DomeneshopDdns + https://psmodule.io/Domeneshop/Functions/Ddns/Update-DomeneshopDdns/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 index 53c6c10..afda6d1 100644 --- a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Add-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Add-DomeneshopDnsRecord + https://psmodule.io/Domeneshop/Functions/Dns/Add-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 index 0634a7b..a96c826 100644 --- a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Get-DomeneshopDnsRecord + https://psmodule.io/Domeneshop/Functions/Dns/Get-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 index 0ea757d..22a8e97 100644 --- a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Remove-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Remove-DomeneshopDnsRecord + https://psmodule.io/Domeneshop/Functions/Dns/Remove-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 index ca92e02..1dc79b9 100644 --- a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Set-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Set-DomeneshopDnsRecord + https://psmodule.io/Domeneshop/Functions/Dns/Set-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 index 214943b..61c22df 100644 --- a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 +++ b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 @@ -28,7 +28,7 @@ function Get-DomeneshopDomain { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Domains/Get-DomeneshopDomain + https://psmodule.io/Domeneshop/Functions/Domains/Get-DomeneshopDomain/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 index 339366f..ed3a649 100644 --- a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 @@ -29,7 +29,7 @@ function Add-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Add-DomeneshopForward + https://psmodule.io/Domeneshop/Functions/Forwards/Add-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 index dfd0494..d782548 100644 --- a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Get-DomeneshopForward + https://psmodule.io/Domeneshop/Functions/Forwards/Get-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 index 63e24f7..866e0d1 100644 --- a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 @@ -25,7 +25,7 @@ function Remove-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Remove-DomeneshopForward + https://psmodule.io/Domeneshop/Functions/Forwards/Remove-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 index 73abb02..c1ea904 100644 --- a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 @@ -29,7 +29,7 @@ function Set-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Set-DomeneshopForward + https://psmodule.io/Domeneshop/Functions/Forwards/Set-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Get-PSModuleTest.ps1 b/src/functions/public/Get-PSModuleTest.ps1 index 1c26ba0..0ec0397 100644 --- a/src/functions/public/Get-PSModuleTest.ps1 +++ b/src/functions/public/Get-PSModuleTest.ps1 @@ -25,7 +25,7 @@ This command is retained as the module's baseline smoke-test command. .LINK - https://psmodule.io/Domeneshop/Functions/Get-PSModuleTest + https://psmodule.io/Domeneshop/Functions/Get-PSModuleTest/ .LINK https://github.com/PSModule/Domeneshop diff --git a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 index 64bef06..cc240cb 100644 --- a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 +++ b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopInvoice { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Invoices/Get-DomeneshopInvoice + https://psmodule.io/Domeneshop/Functions/Invoices/Get-DomeneshopInvoice/ .LINK https://api.domeneshop.no/docs/ diff --git a/tests/PublicHelpLinks.Tests.ps1 b/tests/PublicHelpLinks.Tests.ps1 index 9a34345..c97247f 100644 --- a/tests/PublicHelpLinks.Tests.ps1 +++ b/tests/PublicHelpLinks.Tests.ps1 @@ -36,6 +36,6 @@ Describe 'Public function help links' { $links.Count | Should -BeGreaterThan 0 $links[0].Groups['Uri'].Value | - Should -Be "https://psmodule.io/Domeneshop/Functions/$DocumentationPath" + Should -Be "https://psmodule.io/Domeneshop/Functions/$DocumentationPath/" } } From 4bcdb897dc93febcbf5f6e0741ef132e3a4bcc4d Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 23:48:23 +0200 Subject: [PATCH 35/49] Restore documented no-slash help URLs Reverts the concurrent trailing-slash change to follow the latest canonical standards correction. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/public/Auth/Connect-DomeneshopAccount.ps1 | 2 +- src/functions/public/Auth/Get-DomeneshopContext.ps1 | 2 +- src/functions/public/Ddns/Update-DomeneshopDdns.ps1 | 2 +- src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Domains/Get-DomeneshopDomain.ps1 | 2 +- src/functions/public/Forwards/Add-DomeneshopForward.ps1 | 2 +- src/functions/public/Forwards/Get-DomeneshopForward.ps1 | 2 +- src/functions/public/Forwards/Remove-DomeneshopForward.ps1 | 2 +- src/functions/public/Forwards/Set-DomeneshopForward.ps1 | 2 +- src/functions/public/Get-PSModuleTest.ps1 | 2 +- src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 | 2 +- tests/PublicHelpLinks.Tests.ps1 | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index ea31a40..a48fb49 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -34,7 +34,7 @@ function Connect-DomeneshopAccount { Credentials are encrypted by the Context module. .LINK - https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount/ + https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Auth/Get-DomeneshopContext.ps1 b/src/functions/public/Auth/Get-DomeneshopContext.ps1 index 8fe0dac..ab7e64c 100644 --- a/src/functions/public/Auth/Get-DomeneshopContext.ps1 +++ b/src/functions/public/Auth/Get-DomeneshopContext.ps1 @@ -32,7 +32,7 @@ function Get-DomeneshopContext { The configuration entry used to track the default is excluded from list output. .LINK - https://psmodule.io/Domeneshop/Functions/Auth/Get-DomeneshopContext/ + https://psmodule.io/Domeneshop/Functions/Auth/Get-DomeneshopContext .LINK https://psmodule.io/Context/ diff --git a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 index 339f6c3..d05d4ef 100644 --- a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 +++ b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 @@ -25,7 +25,7 @@ function Update-DomeneshopDdns { Uses the caller's public IP address when MyIP is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Ddns/Update-DomeneshopDdns/ + https://psmodule.io/Domeneshop/Functions/Ddns/Update-DomeneshopDdns .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 index afda6d1..53c6c10 100644 --- a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Add-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Add-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Add-DomeneshopDnsRecord .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 index a96c826..0634a7b 100644 --- a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Get-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Get-DomeneshopDnsRecord .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 index 22a8e97..0ea757d 100644 --- a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Remove-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Remove-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Remove-DomeneshopDnsRecord .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 index 1dc79b9..ca92e02 100644 --- a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Set-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Set-DomeneshopDnsRecord/ + https://psmodule.io/Domeneshop/Functions/Dns/Set-DomeneshopDnsRecord .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 index 61c22df..214943b 100644 --- a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 +++ b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 @@ -28,7 +28,7 @@ function Get-DomeneshopDomain { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Domains/Get-DomeneshopDomain/ + https://psmodule.io/Domeneshop/Functions/Domains/Get-DomeneshopDomain .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 index ed3a649..339366f 100644 --- a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 @@ -29,7 +29,7 @@ function Add-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Add-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Add-DomeneshopForward .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 index d782548..dfd0494 100644 --- a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Get-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Get-DomeneshopForward .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 index 866e0d1..63e24f7 100644 --- a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 @@ -25,7 +25,7 @@ function Remove-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Remove-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Remove-DomeneshopForward .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 index c1ea904..73abb02 100644 --- a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 @@ -29,7 +29,7 @@ function Set-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Set-DomeneshopForward/ + https://psmodule.io/Domeneshop/Functions/Forwards/Set-DomeneshopForward .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Get-PSModuleTest.ps1 b/src/functions/public/Get-PSModuleTest.ps1 index 0ec0397..1c26ba0 100644 --- a/src/functions/public/Get-PSModuleTest.ps1 +++ b/src/functions/public/Get-PSModuleTest.ps1 @@ -25,7 +25,7 @@ This command is retained as the module's baseline smoke-test command. .LINK - https://psmodule.io/Domeneshop/Functions/Get-PSModuleTest/ + https://psmodule.io/Domeneshop/Functions/Get-PSModuleTest .LINK https://github.com/PSModule/Domeneshop diff --git a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 index cc240cb..64bef06 100644 --- a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 +++ b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopInvoice { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Invoices/Get-DomeneshopInvoice/ + https://psmodule.io/Domeneshop/Functions/Invoices/Get-DomeneshopInvoice .LINK https://api.domeneshop.no/docs/ diff --git a/tests/PublicHelpLinks.Tests.ps1 b/tests/PublicHelpLinks.Tests.ps1 index c97247f..9a34345 100644 --- a/tests/PublicHelpLinks.Tests.ps1 +++ b/tests/PublicHelpLinks.Tests.ps1 @@ -36,6 +36,6 @@ Describe 'Public function help links' { $links.Count | Should -BeGreaterThan 0 $links[0].Groups['Uri'].Value | - Should -Be "https://psmodule.io/Domeneshop/Functions/$DocumentationPath/" + Should -Be "https://psmodule.io/Domeneshop/Functions/$DocumentationPath" } } From 066d50a3ce57c810dade56b167e8d041078654f1 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sat, 25 Jul 2026 23:57:24 +0200 Subject: [PATCH 36/49] Keep help-link tests compatible with PowerShell 5.1 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/PublicHelpLinks.Tests.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/PublicHelpLinks.Tests.ps1 b/tests/PublicHelpLinks.Tests.ps1 index 9a34345..dec505a 100644 --- a/tests/PublicHelpLinks.Tests.ps1 +++ b/tests/PublicHelpLinks.Tests.ps1 @@ -13,8 +13,9 @@ $publicFunctionsPath = Join-Path -Path $publicFunctionsPath -ChildPath 'public' $testCases = Get-ChildItem -Path $publicFunctionsPath -Filter '*.ps1' -Recurse -File | Sort-Object -Property FullName | ForEach-Object { - $relativePath = [IO.Path]::GetRelativePath($publicFunctionsPath, $_.FullName) - $relativeDirectory = Split-Path -Path $relativePath -Parent + $relativeDirectory = $_.DirectoryName.Remove(0, $publicFunctionsPath.Length).TrimStart( + [char[]] @([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + ) $documentationPath = if ($relativeDirectory) { '{0}/{1}' -f ($relativeDirectory -replace '[\\/]', '/'), $_.BaseName } else { From ebf73206c1ac83bd6ef4c453aaf4910177b462f5 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 00:03:03 +0200 Subject: [PATCH 37/49] Target the latest PowerShell LTS Remove legacy Windows PowerShell compatibility paths from API transport and help-link validation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../private/Invoke-DomeneshopApiRequest.ps1 | 13 +++++-------- tests/PublicHelpLinks.Tests.ps1 | 5 ++--- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/functions/private/Invoke-DomeneshopApiRequest.ps1 b/src/functions/private/Invoke-DomeneshopApiRequest.ps1 index 49c19ee..3cfd191 100644 --- a/src/functions/private/Invoke-DomeneshopApiRequest.ps1 +++ b/src/functions/private/Invoke-DomeneshopApiRequest.ps1 @@ -62,14 +62,11 @@ function Invoke-DomeneshopApiRequest { ) $params = @{ - Method = $Method - Uri = $Uri - Credential = $credential - ErrorAction = 'Stop' - } - - if ($PSVersionTable.PSVersion.Major -ge 6) { - $params['Authentication'] = 'Basic' + Method = $Method + Uri = $Uri + Authentication = 'Basic' + Credential = $credential + ErrorAction = 'Stop' } if ($PSBoundParameters.ContainsKey('Body')) { diff --git a/tests/PublicHelpLinks.Tests.ps1 b/tests/PublicHelpLinks.Tests.ps1 index dec505a..9a34345 100644 --- a/tests/PublicHelpLinks.Tests.ps1 +++ b/tests/PublicHelpLinks.Tests.ps1 @@ -13,9 +13,8 @@ $publicFunctionsPath = Join-Path -Path $publicFunctionsPath -ChildPath 'public' $testCases = Get-ChildItem -Path $publicFunctionsPath -Filter '*.ps1' -Recurse -File | Sort-Object -Property FullName | ForEach-Object { - $relativeDirectory = $_.DirectoryName.Remove(0, $publicFunctionsPath.Length).TrimStart( - [char[]] @([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) - ) + $relativePath = [IO.Path]::GetRelativePath($publicFunctionsPath, $_.FullName) + $relativeDirectory = Split-Path -Path $relativePath -Parent $documentationPath = if ($relativeDirectory) { '{0}/{1}' -f ($relativeDirectory -replace '[\\/]', '/'), $_.BaseName } else { From e7385a480c64c3160841bf9f2d63784bfec204ab Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 00:09:47 +0200 Subject: [PATCH 38/49] Reject whitespace domain filters Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/public/Domains/Get-DomeneshopDomain.ps1 | 1 + tests/Get-DomeneshopDomain.Tests.ps1 | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 index 214943b..7754b20 100644 --- a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 +++ b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 @@ -39,6 +39,7 @@ function Get-DomeneshopDomain { # A domain-name filter for list requests. [Parameter(ParameterSetName = 'List')] [ValidateNotNullOrEmpty()] + [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] [string] $Domain, # The numeric identifier of a specific domain. diff --git a/tests/Get-DomeneshopDomain.Tests.ps1 b/tests/Get-DomeneshopDomain.Tests.ps1 index 3a5b8ed..deb77f4 100644 --- a/tests/Get-DomeneshopDomain.Tests.ps1 +++ b/tests/Get-DomeneshopDomain.Tests.ps1 @@ -42,4 +42,10 @@ Describe 'Get-DomeneshopDomain' { It 'rejects non-positive domain IDs' { { Get-DomeneshopDomain -Context 'demo' -DomainID 0 } | Should -Throw } + + It 'rejects whitespace-only domain filters' { + { Get-DomeneshopDomain -Context 'demo' -Domain ' ' } | Should -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } } From f8711c8ab7c8fad8830d6f17172cd45cf9979bba Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 00:15:41 +0200 Subject: [PATCH 39/49] Reject whitespace-only command inputs Cover API tokens, DDNS parameters, DNS filters, forward hosts, and smoke-test names at the parameter boundary. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/public/Auth/Connect-DomeneshopAccount.ps1 | 1 + src/functions/public/Ddns/Update-DomeneshopDdns.ps1 | 2 ++ src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 | 3 +++ src/functions/public/Forwards/Get-DomeneshopForward.ps1 | 1 + .../public/Forwards/Remove-DomeneshopForward.ps1 | 1 + src/functions/public/Forwards/Set-DomeneshopForward.ps1 | 1 + src/functions/public/Get-PSModuleTest.ps1 | 1 + tests/Connect-DomeneshopAccount.Tests.ps1 | 6 ++++++ tests/Get-DomeneshopDnsRecord.Tests.ps1 | 8 ++++++++ tests/Get-DomeneshopForward.Tests.ps1 | 6 ++++++ tests/Get-PSModuleTest.Tests.ps1 | 4 ++++ tests/Remove-DomeneshopForward.Tests.ps1 | 6 ++++++ tests/Set-DomeneshopForward.Tests.ps1 | 8 ++++++++ tests/Update-DomeneshopDdns.Tests.ps1 | 7 +++++++ 14 files changed, 55 insertions(+) diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index a48fb49..e3e8682 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -49,6 +49,7 @@ function Connect-DomeneshopAccount { # The Domeneshop API token used as the Basic authentication username. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] + [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] [string] $Token, # The Domeneshop API secret as a string or secure string. diff --git a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 index d05d4ef..bc3413d 100644 --- a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 +++ b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 @@ -40,12 +40,14 @@ function Update-DomeneshopDdns { # The fully qualified hostname to update. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] + [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] [Alias('Host')] [string] $Hostname, # The IPv4 or IPv6 address to publish. [Parameter()] [ValidateNotNullOrEmpty()] + [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] [Alias('IP')] [string] $MyIP, diff --git a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 index 0634a7b..95b1854 100644 --- a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 @@ -51,17 +51,20 @@ function Get-DomeneshopDnsRecord { # A hostname filter for list requests. [Parameter(ParameterSetName = 'List')] [ValidateNotNullOrEmpty()] + [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] [Alias('Host')] [string] $RecordHost, # A DNS record type filter for list requests. [Parameter(ParameterSetName = 'List')] [ValidateNotNullOrEmpty()] + [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] [string] $Type, # A record-data filter for list requests. [Parameter(ParameterSetName = 'List')] [ValidateNotNullOrEmpty()] + [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] [string] $Data, # The stored credential context to use instead of the default. diff --git a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 index dfd0494..bbe0df4 100644 --- a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 @@ -46,6 +46,7 @@ function Get-DomeneshopForward { # The hostname of a specific HTTP forward. [Parameter(Mandatory, ParameterSetName = 'Get by host')] [ValidateNotNullOrEmpty()] + [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] [Alias('Host')] [string] $ForwardHost, diff --git a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 index 63e24f7..467a3e7 100644 --- a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 @@ -41,6 +41,7 @@ function Remove-DomeneshopForward { # The hostname of the HTTP forward to remove. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] + [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] [Alias('Host')] [string] $ForwardHost, diff --git a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 index 73abb02..8487cfb 100644 --- a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 @@ -45,6 +45,7 @@ function Set-DomeneshopForward { # The hostname of the HTTP forward to update. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] + [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] [Alias('Host')] [string] $ForwardHost, diff --git a/src/functions/public/Get-PSModuleTest.ps1 b/src/functions/public/Get-PSModuleTest.ps1 index 1c26ba0..177454d 100644 --- a/src/functions/public/Get-PSModuleTest.ps1 +++ b/src/functions/public/Get-PSModuleTest.ps1 @@ -36,6 +36,7 @@ # The name to include in the greeting. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] + [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] [string] $Name ) diff --git a/tests/Connect-DomeneshopAccount.Tests.ps1 b/tests/Connect-DomeneshopAccount.Tests.ps1 index 378a712..282a4a4 100644 --- a/tests/Connect-DomeneshopAccount.Tests.ps1 +++ b/tests/Connect-DomeneshopAccount.Tests.ps1 @@ -109,4 +109,10 @@ Describe 'Connect-DomeneshopAccount' { Should -Invoke Set-Context -Times 0 -Exactly } + + It 'rejects a whitespace-only API token' { + { Connect-DomeneshopAccount -Token ' ' -Secret 'secret' -Context 'demo' } | Should -Throw + + Should -Invoke Set-Context -Times 0 -Exactly + } } diff --git a/tests/Get-DomeneshopDnsRecord.Tests.ps1 b/tests/Get-DomeneshopDnsRecord.Tests.ps1 index ca7dd9e..3db699d 100644 --- a/tests/Get-DomeneshopDnsRecord.Tests.ps1 +++ b/tests/Get-DomeneshopDnsRecord.Tests.ps1 @@ -38,4 +38,12 @@ Describe 'Get-DomeneshopDnsRecord' { $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' } } + + It 'rejects whitespace-only list filters' { + { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordHost ' ' } | Should -Throw + { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Type ' ' } | Should -Throw + { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Data ' ' } | Should -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } } diff --git a/tests/Get-DomeneshopForward.Tests.ps1 b/tests/Get-DomeneshopForward.Tests.ps1 index d1b6db3..2abe933 100644 --- a/tests/Get-DomeneshopForward.Tests.ps1 +++ b/tests/Get-DomeneshopForward.Tests.ps1 @@ -38,4 +38,10 @@ Describe 'Get-DomeneshopForward' { $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/home%20office' } } + + It 'rejects whitespace-only forward hosts' { + { Get-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' } | Should -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } } diff --git a/tests/Get-PSModuleTest.Tests.ps1 b/tests/Get-PSModuleTest.Tests.ps1 index f46c993..557df8c 100644 --- a/tests/Get-PSModuleTest.Tests.ps1 +++ b/tests/Get-PSModuleTest.Tests.ps1 @@ -19,4 +19,8 @@ Describe 'Get-PSModuleTest' { It 'returns a greeting for the supplied name' { Get-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' } + + It 'rejects a whitespace-only name' { + { Get-PSModuleTest -Name ' ' } | Should -Throw + } } diff --git a/tests/Remove-DomeneshopForward.Tests.ps1 b/tests/Remove-DomeneshopForward.Tests.ps1 index ca6f8cc..fd375af 100644 --- a/tests/Remove-DomeneshopForward.Tests.ps1 +++ b/tests/Remove-DomeneshopForward.Tests.ps1 @@ -35,4 +35,10 @@ Describe 'Remove-DomeneshopForward' { Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly } + + It 'rejects whitespace-only forward hosts' { + { Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' -Confirm:$false } | Should -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } } diff --git a/tests/Set-DomeneshopForward.Tests.ps1 b/tests/Set-DomeneshopForward.Tests.ps1 index eae2d43..a4cae34 100644 --- a/tests/Set-DomeneshopForward.Tests.ps1 +++ b/tests/Set-DomeneshopForward.Tests.ps1 @@ -37,4 +37,12 @@ Describe 'Set-DomeneshopForward' { Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly } + + It 'rejects whitespace-only forward hosts' { + { + Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' -Forward $script:Forward + } | Should -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } } diff --git a/tests/Update-DomeneshopDdns.Tests.ps1 b/tests/Update-DomeneshopDdns.Tests.ps1 index 91beba0..597d42e 100644 --- a/tests/Update-DomeneshopDdns.Tests.ps1 +++ b/tests/Update-DomeneshopDdns.Tests.ps1 @@ -35,4 +35,11 @@ Describe 'Update-DomeneshopDdns' { Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly } + + It 'rejects whitespace-only hostnames and IP addresses' { + { Update-DomeneshopDdns -Context 'demo' -Hostname ' ' } | Should -Throw + { Update-DomeneshopDdns -Context 'demo' -Hostname 'home.example.com' -MyIP ' ' } | Should -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } } From 0c36880e20daa0886164e4f648c93c51bacd5a9d Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 00:22:35 +0200 Subject: [PATCH 40/49] Require the latest PowerShell LTS Declare PowerShell 7.6 so generated module metadata enforces the supported runtime and add deterministic policy coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../public/Auth/Connect-DomeneshopAccount.ps1 | 1 + tests/ModuleRequirements.Tests.ps1 | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 tests/ModuleRequirements.Tests.ps1 diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index e3e8682..8310652 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -1,3 +1,4 @@ +#Requires -Version 7.6 #Requires -Modules @{ ModuleName = 'Context'; ModuleVersion = '8.1.6' } function Connect-DomeneshopAccount { diff --git a/tests/ModuleRequirements.Tests.ps1 b/tests/ModuleRequirements.Tests.ps1 new file mode 100644 index 0000000..66b153c --- /dev/null +++ b/tests/ModuleRequirements.Tests.ps1 @@ -0,0 +1,29 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test discovery.' +)] +[CmdletBinding()] +param() + +$sourcePath = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'src' +$testCases = @( + @{ + RequiredVersions = @( + Get-ChildItem -Path $sourcePath -Filter '*.ps1' -Recurse -File | + Select-String -Pattern '^\s*#Requires\s+-Version\s+(?\S+)\s*$' | + ForEach-Object { $_.Matches[0].Groups['Version'].Value } | + Sort-Object -Unique + ) + } +) + +Describe 'Module requirements' { + It 'targets only the latest PowerShell LTS' -ForEach $testCases { + param($RequiredVersions) + + $RequiredVersions | Should -HaveCount 1 + $RequiredVersions[0] | Should -Be '7.6' + } +} From 41f39ca521652c022aaa2ca940adf45f81067a2e Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 00:28:31 +0200 Subject: [PATCH 41/49] Use canonical generated help routes Keep group-aware paths and require the trailing-slash form for every public function's first help link. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/public/Auth/Connect-DomeneshopAccount.ps1 | 2 +- src/functions/public/Auth/Get-DomeneshopContext.ps1 | 2 +- src/functions/public/Ddns/Update-DomeneshopDdns.ps1 | 2 +- src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 | 2 +- src/functions/public/Domains/Get-DomeneshopDomain.ps1 | 2 +- src/functions/public/Forwards/Add-DomeneshopForward.ps1 | 2 +- src/functions/public/Forwards/Get-DomeneshopForward.ps1 | 2 +- src/functions/public/Forwards/Remove-DomeneshopForward.ps1 | 2 +- src/functions/public/Forwards/Set-DomeneshopForward.ps1 | 2 +- src/functions/public/Get-PSModuleTest.ps1 | 2 +- src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 | 2 +- tests/PublicHelpLinks.Tests.ps1 | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index 8310652..353fae0 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -35,7 +35,7 @@ function Connect-DomeneshopAccount { Credentials are encrypted by the Context module. .LINK - https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount + https://psmodule.io/Domeneshop/Functions/Auth/Connect-DomeneshopAccount/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Auth/Get-DomeneshopContext.ps1 b/src/functions/public/Auth/Get-DomeneshopContext.ps1 index ab7e64c..8fe0dac 100644 --- a/src/functions/public/Auth/Get-DomeneshopContext.ps1 +++ b/src/functions/public/Auth/Get-DomeneshopContext.ps1 @@ -32,7 +32,7 @@ function Get-DomeneshopContext { The configuration entry used to track the default is excluded from list output. .LINK - https://psmodule.io/Domeneshop/Functions/Auth/Get-DomeneshopContext + https://psmodule.io/Domeneshop/Functions/Auth/Get-DomeneshopContext/ .LINK https://psmodule.io/Context/ diff --git a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 index bc3413d..8d1e51a 100644 --- a/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 +++ b/src/functions/public/Ddns/Update-DomeneshopDdns.ps1 @@ -25,7 +25,7 @@ function Update-DomeneshopDdns { Uses the caller's public IP address when MyIP is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Ddns/Update-DomeneshopDdns + https://psmodule.io/Domeneshop/Functions/Ddns/Update-DomeneshopDdns/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 index 53c6c10..afda6d1 100644 --- a/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Add-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Add-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Add-DomeneshopDnsRecord + https://psmodule.io/Domeneshop/Functions/Dns/Add-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 index 95b1854..5ff103d 100644 --- a/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Get-DomeneshopDnsRecord.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Get-DomeneshopDnsRecord + https://psmodule.io/Domeneshop/Functions/Dns/Get-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 index 0ea757d..22a8e97 100644 --- a/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Remove-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Remove-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Remove-DomeneshopDnsRecord + https://psmodule.io/Domeneshop/Functions/Dns/Remove-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 index ca92e02..1dc79b9 100644 --- a/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 +++ b/src/functions/public/Dns/Set-DomeneshopDnsRecord.ps1 @@ -25,7 +25,7 @@ function Set-DomeneshopDnsRecord { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Dns/Set-DomeneshopDnsRecord + https://psmodule.io/Domeneshop/Functions/Dns/Set-DomeneshopDnsRecord/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 index 7754b20..6fba6e0 100644 --- a/src/functions/public/Domains/Get-DomeneshopDomain.ps1 +++ b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 @@ -28,7 +28,7 @@ function Get-DomeneshopDomain { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Domains/Get-DomeneshopDomain + https://psmodule.io/Domeneshop/Functions/Domains/Get-DomeneshopDomain/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 index 339366f..ed3a649 100644 --- a/src/functions/public/Forwards/Add-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Add-DomeneshopForward.ps1 @@ -29,7 +29,7 @@ function Add-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Add-DomeneshopForward + https://psmodule.io/Domeneshop/Functions/Forwards/Add-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 index bbe0df4..a320dba 100644 --- a/src/functions/public/Forwards/Get-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Get-DomeneshopForward.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Get-DomeneshopForward + https://psmodule.io/Domeneshop/Functions/Forwards/Get-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 index 467a3e7..f83b1c1 100644 --- a/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Remove-DomeneshopForward.ps1 @@ -25,7 +25,7 @@ function Remove-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Remove-DomeneshopForward + https://psmodule.io/Domeneshop/Functions/Forwards/Remove-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 index 8487cfb..98d0bb3 100644 --- a/src/functions/public/Forwards/Set-DomeneshopForward.ps1 +++ b/src/functions/public/Forwards/Set-DomeneshopForward.ps1 @@ -29,7 +29,7 @@ function Set-DomeneshopForward { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Forwards/Set-DomeneshopForward + https://psmodule.io/Domeneshop/Functions/Forwards/Set-DomeneshopForward/ .LINK https://api.domeneshop.no/docs/ diff --git a/src/functions/public/Get-PSModuleTest.ps1 b/src/functions/public/Get-PSModuleTest.ps1 index 177454d..d8a806d 100644 --- a/src/functions/public/Get-PSModuleTest.ps1 +++ b/src/functions/public/Get-PSModuleTest.ps1 @@ -25,7 +25,7 @@ This command is retained as the module's baseline smoke-test command. .LINK - https://psmodule.io/Domeneshop/Functions/Get-PSModuleTest + https://psmodule.io/Domeneshop/Functions/Get-PSModuleTest/ .LINK https://github.com/PSModule/Domeneshop diff --git a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 index 64bef06..cc240cb 100644 --- a/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 +++ b/src/functions/public/Invoices/Get-DomeneshopInvoice.ps1 @@ -30,7 +30,7 @@ function Get-DomeneshopInvoice { Uses the default context when Context is omitted. .LINK - https://psmodule.io/Domeneshop/Functions/Invoices/Get-DomeneshopInvoice + https://psmodule.io/Domeneshop/Functions/Invoices/Get-DomeneshopInvoice/ .LINK https://api.domeneshop.no/docs/ diff --git a/tests/PublicHelpLinks.Tests.ps1 b/tests/PublicHelpLinks.Tests.ps1 index 9a34345..c97247f 100644 --- a/tests/PublicHelpLinks.Tests.ps1 +++ b/tests/PublicHelpLinks.Tests.ps1 @@ -36,6 +36,6 @@ Describe 'Public function help links' { $links.Count | Should -BeGreaterThan 0 $links[0].Groups['Uri'].Value | - Should -Be "https://psmodule.io/Domeneshop/Functions/$DocumentationPath" + Should -Be "https://psmodule.io/Domeneshop/Functions/$DocumentationPath/" } } From a8e167da9454e6ec7753bad6298e0523158c1ba9 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 00:35:18 +0200 Subject: [PATCH 42/49] Keep credential prompting available without a browser Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../public/Auth/Connect-DomeneshopAccount.ps1 | 6 +++++- tests/Connect-DomeneshopAccount.Tests.ps1 | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 index 353fae0..0569664 100644 --- a/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 +++ b/src/functions/public/Auth/Connect-DomeneshopAccount.ps1 @@ -77,7 +77,11 @@ function Connect-DomeneshopAccount { if ($PSCmdlet.ShouldProcess("Domeneshop context [$Context]", 'Store API credentials')) { if (-not $PSBoundParameters.ContainsKey('Secret')) { - Start-Process -FilePath 'https://domene.shop/admin?view=api' + try { + Start-Process -FilePath 'https://domene.shop/admin?view=api' + } catch { + Write-Warning "Unable to open the Domeneshop API settings page: $($_.Exception.Message)" + } $Secret = Read-Host -Prompt 'Enter the Domeneshop API secret' -AsSecureString $null = Test-DomeneshopSecret -Secret $Secret } diff --git a/tests/Connect-DomeneshopAccount.Tests.ps1 b/tests/Connect-DomeneshopAccount.Tests.ps1 index 282a4a4..b427e38 100644 --- a/tests/Connect-DomeneshopAccount.Tests.ps1 +++ b/tests/Connect-DomeneshopAccount.Tests.ps1 @@ -64,6 +64,23 @@ Describe 'Connect-DomeneshopAccount' { } } + It 'securely prompts and stores credentials when the browser cannot open' { + Mock Start-Process { throw 'No browser is available.' } + Mock Write-Warning {} + + Connect-DomeneshopAccount -Token 'token' -Context 'demo' + + Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { + $Message -eq 'Unable to open the Domeneshop API settings page: No browser is available.' + } + Should -Invoke Read-Host -Times 1 -Exactly -ParameterFilter { + $Prompt -eq 'Enter the Domeneshop API secret' -and $AsSecureString + } + Should -Invoke Set-Context -Times 1 -Exactly -ParameterFilter { + [object]::ReferenceEquals($Context.Secret, $script:PromptedSecret) + } + } + It 'does not open API settings or prompt when WhatIf omits Secret' { Connect-DomeneshopAccount -Token 'token' -Context 'demo' -WhatIf From b8663612eb80322fa9c0de9e12fd8870430f914e Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 00:59:37 +0200 Subject: [PATCH 43/49] Update Process-PSModule to v6.1.13 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/Process-PSModule.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml index 23b436c..f2b9b43 100644 --- a/.github/workflows/Process-PSModule.yml +++ b/.github/workflows/Process-PSModule.yml @@ -27,6 +27,6 @@ permissions: jobs: Process-PSModule: - uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@da180bac16b13bfbcdf08b2e4e221b5b49e5ff28 # v6.1.4 + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@fb1bdb8fefd243292f779d2a856a38db6fe6daf4 # v6.1.13 secrets: APIKEY: ${{ secrets.APIKEY }} From a32219c24f4f3e482c1f9b304feeacd632cd8254 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 01:01:20 +0200 Subject: [PATCH 44/49] Group public command tests by feature Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Connect-DomeneshopAccount.Tests.ps1 | 2 +- .../Get-DomeneshopContext.Tests.ps1 | 2 +- .../Update-DomeneshopDdns.Tests.ps1 | 2 +- .../Add-DomeneshopDnsRecord.Tests.ps1 | 2 +- .../Get-DomeneshopDnsRecord.Tests.ps1 | 2 +- .../Remove-DomeneshopDnsRecord.Tests.ps1 | 2 +- .../Set-DomeneshopDnsRecord.Tests.ps1 | 2 +- .../Get-DomeneshopDomain.Tests.ps1 | 2 +- .../Add-DomeneshopForward.Tests.ps1 | 2 +- .../Get-DomeneshopForward.Tests.ps1 | 2 +- .../Remove-DomeneshopForward.Tests.ps1 | 2 +- .../Set-DomeneshopForward.Tests.ps1 | 2 +- .../Get-DomeneshopInvoice.Tests.ps1 | 2 +- tests/TestLayout.Tests.ps1 | 23 +++++++++++++++++++ 14 files changed, 36 insertions(+), 13 deletions(-) rename tests/{ => Auth}/Connect-DomeneshopAccount.Tests.ps1 (98%) rename tests/{ => Auth}/Get-DomeneshopContext.Tests.ps1 (95%) rename tests/{ => Ddns}/Update-DomeneshopDdns.Tests.ps1 (93%) rename tests/{ => Dns}/Add-DomeneshopDnsRecord.Tests.ps1 (92%) rename tests/{ => Dns}/Get-DomeneshopDnsRecord.Tests.ps1 (94%) rename tests/{ => Dns}/Remove-DomeneshopDnsRecord.Tests.ps1 (91%) rename tests/{ => Dns}/Set-DomeneshopDnsRecord.Tests.ps1 (92%) rename tests/{ => Domains}/Get-DomeneshopDomain.Tests.ps1 (93%) rename tests/{ => Forwards}/Add-DomeneshopForward.Tests.ps1 (92%) rename tests/{ => Forwards}/Get-DomeneshopForward.Tests.ps1 (93%) rename tests/{ => Forwards}/Remove-DomeneshopForward.Tests.ps1 (93%) rename tests/{ => Forwards}/Set-DomeneshopForward.Tests.ps1 (93%) rename tests/{ => Invoices}/Get-DomeneshopInvoice.Tests.ps1 (91%) create mode 100644 tests/TestLayout.Tests.ps1 diff --git a/tests/Connect-DomeneshopAccount.Tests.ps1 b/tests/Auth/Connect-DomeneshopAccount.Tests.ps1 similarity index 98% rename from tests/Connect-DomeneshopAccount.Tests.ps1 rename to tests/Auth/Connect-DomeneshopAccount.Tests.ps1 index b427e38..c253034 100644 --- a/tests/Connect-DomeneshopAccount.Tests.ps1 +++ b/tests/Auth/Connect-DomeneshopAccount.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Connect-DomeneshopAccount' { BeforeAll { - . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Get-DomeneshopContext.Tests.ps1 b/tests/Auth/Get-DomeneshopContext.Tests.ps1 similarity index 95% rename from tests/Get-DomeneshopContext.Tests.ps1 rename to tests/Auth/Get-DomeneshopContext.Tests.ps1 index 236d74e..d621529 100644 --- a/tests/Get-DomeneshopContext.Tests.ps1 +++ b/tests/Auth/Get-DomeneshopContext.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Get-DomeneshopContext' { BeforeAll { - . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') } It 'gets the configured default context' { diff --git a/tests/Update-DomeneshopDdns.Tests.ps1 b/tests/Ddns/Update-DomeneshopDdns.Tests.ps1 similarity index 93% rename from tests/Update-DomeneshopDdns.Tests.ps1 rename to tests/Ddns/Update-DomeneshopDdns.Tests.ps1 index 597d42e..33bf9eb 100644 --- a/tests/Update-DomeneshopDdns.Tests.ps1 +++ b/tests/Ddns/Update-DomeneshopDdns.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Update-DomeneshopDdns' { BeforeAll { - . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Add-DomeneshopDnsRecord.Tests.ps1 b/tests/Dns/Add-DomeneshopDnsRecord.Tests.ps1 similarity index 92% rename from tests/Add-DomeneshopDnsRecord.Tests.ps1 rename to tests/Dns/Add-DomeneshopDnsRecord.Tests.ps1 index 2699a4b..0d952a2 100644 --- a/tests/Add-DomeneshopDnsRecord.Tests.ps1 +++ b/tests/Dns/Add-DomeneshopDnsRecord.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Add-DomeneshopDnsRecord' { BeforeAll { - . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Get-DomeneshopDnsRecord.Tests.ps1 b/tests/Dns/Get-DomeneshopDnsRecord.Tests.ps1 similarity index 94% rename from tests/Get-DomeneshopDnsRecord.Tests.ps1 rename to tests/Dns/Get-DomeneshopDnsRecord.Tests.ps1 index 3db699d..b30384b 100644 --- a/tests/Get-DomeneshopDnsRecord.Tests.ps1 +++ b/tests/Dns/Get-DomeneshopDnsRecord.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Get-DomeneshopDnsRecord' { BeforeAll { - . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Remove-DomeneshopDnsRecord.Tests.ps1 b/tests/Dns/Remove-DomeneshopDnsRecord.Tests.ps1 similarity index 91% rename from tests/Remove-DomeneshopDnsRecord.Tests.ps1 rename to tests/Dns/Remove-DomeneshopDnsRecord.Tests.ps1 index fc02fba..043fb6a 100644 --- a/tests/Remove-DomeneshopDnsRecord.Tests.ps1 +++ b/tests/Dns/Remove-DomeneshopDnsRecord.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Remove-DomeneshopDnsRecord' { BeforeAll { - . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Set-DomeneshopDnsRecord.Tests.ps1 b/tests/Dns/Set-DomeneshopDnsRecord.Tests.ps1 similarity index 92% rename from tests/Set-DomeneshopDnsRecord.Tests.ps1 rename to tests/Dns/Set-DomeneshopDnsRecord.Tests.ps1 index 0626b58..86b7c0f 100644 --- a/tests/Set-DomeneshopDnsRecord.Tests.ps1 +++ b/tests/Dns/Set-DomeneshopDnsRecord.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Set-DomeneshopDnsRecord' { BeforeAll { - . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Get-DomeneshopDomain.Tests.ps1 b/tests/Domains/Get-DomeneshopDomain.Tests.ps1 similarity index 93% rename from tests/Get-DomeneshopDomain.Tests.ps1 rename to tests/Domains/Get-DomeneshopDomain.Tests.ps1 index deb77f4..a8d2a1b 100644 --- a/tests/Get-DomeneshopDomain.Tests.ps1 +++ b/tests/Domains/Get-DomeneshopDomain.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Get-DomeneshopDomain' { BeforeAll { - . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Add-DomeneshopForward.Tests.ps1 b/tests/Forwards/Add-DomeneshopForward.Tests.ps1 similarity index 92% rename from tests/Add-DomeneshopForward.Tests.ps1 rename to tests/Forwards/Add-DomeneshopForward.Tests.ps1 index 9483231..17ecfda 100644 --- a/tests/Add-DomeneshopForward.Tests.ps1 +++ b/tests/Forwards/Add-DomeneshopForward.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Add-DomeneshopForward' { BeforeAll { - . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Get-DomeneshopForward.Tests.ps1 b/tests/Forwards/Get-DomeneshopForward.Tests.ps1 similarity index 93% rename from tests/Get-DomeneshopForward.Tests.ps1 rename to tests/Forwards/Get-DomeneshopForward.Tests.ps1 index 2abe933..dc233eb 100644 --- a/tests/Get-DomeneshopForward.Tests.ps1 +++ b/tests/Forwards/Get-DomeneshopForward.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Get-DomeneshopForward' { BeforeAll { - . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Remove-DomeneshopForward.Tests.ps1 b/tests/Forwards/Remove-DomeneshopForward.Tests.ps1 similarity index 93% rename from tests/Remove-DomeneshopForward.Tests.ps1 rename to tests/Forwards/Remove-DomeneshopForward.Tests.ps1 index fd375af..e5c9f73 100644 --- a/tests/Remove-DomeneshopForward.Tests.ps1 +++ b/tests/Forwards/Remove-DomeneshopForward.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Remove-DomeneshopForward' { BeforeAll { - . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Set-DomeneshopForward.Tests.ps1 b/tests/Forwards/Set-DomeneshopForward.Tests.ps1 similarity index 93% rename from tests/Set-DomeneshopForward.Tests.ps1 rename to tests/Forwards/Set-DomeneshopForward.Tests.ps1 index a4cae34..5037b95 100644 --- a/tests/Set-DomeneshopForward.Tests.ps1 +++ b/tests/Forwards/Set-DomeneshopForward.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Set-DomeneshopForward' { BeforeAll { - . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Get-DomeneshopInvoice.Tests.ps1 b/tests/Invoices/Get-DomeneshopInvoice.Tests.ps1 similarity index 91% rename from tests/Get-DomeneshopInvoice.Tests.ps1 rename to tests/Invoices/Get-DomeneshopInvoice.Tests.ps1 index e879b5c..19bebd9 100644 --- a/tests/Get-DomeneshopInvoice.Tests.ps1 +++ b/tests/Invoices/Get-DomeneshopInvoice.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Get-DomeneshopInvoice' { BeforeAll { - . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/TestLayout.Tests.ps1 b/tests/TestLayout.Tests.ps1 new file mode 100644 index 0000000..6259d1b --- /dev/null +++ b/tests/TestLayout.Tests.ps1 @@ -0,0 +1,23 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + +[CmdletBinding()] +param() + +$sourcePath = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'src' +$publicFunctionsPath = Join-Path -Path $sourcePath -ChildPath 'functions/public' +$testCases = Get-ChildItem -Path $publicFunctionsPath -Filter '*.ps1' -Recurse | + ForEach-Object { + $relativePath = [IO.Path]::GetRelativePath($publicFunctionsPath, $_.FullName) + @{ + FunctionName = $_.BaseName + TestPath = Join-Path -Path $PSScriptRoot -ChildPath ( + [IO.Path]::ChangeExtension($relativePath, '.Tests.ps1') + ) + } + } + +Describe 'Public command test layout' { + It 'mirrors the public source path for ' -ForEach $testCases { + Test-Path -LiteralPath $TestPath -PathType Leaf | Should -BeTrue + } +} From 3e045a50059cdee24995780888c60e52cef19f36 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 01:06:44 +0200 Subject: [PATCH 45/49] Migrate module docs configuration to Zensical Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/mkdocs.yml | 81 ------------------------------------------- .github/zensical.toml | 69 ++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 81 deletions(-) delete mode 100644 .github/mkdocs.yml create mode 100644 .github/zensical.toml diff --git a/.github/mkdocs.yml b/.github/mkdocs.yml deleted file mode 100644 index 67bd37a..0000000 --- a/.github/mkdocs.yml +++ /dev/null @@ -1,81 +0,0 @@ -# The '-{{ ... }}-'' are variables that are replaced during deployment, so no need to change. -# Rest of the settings can be changed as per your requirements. -# -# References: -# - https://squidfunk.github.io/mkdocs-material/setup/ - -site_name: -{{ REPO_NAME }}- -theme: - name: material - language: en - font: - text: Roboto - code: Sono - logo: Assets/icon.png - favicon: Assets/icon.png - palette: - # Palette toggle for automatic mode - - media: "(prefers-color-scheme)" - toggle: - icon: material/link - name: Switch to dark mode - # Palette toggle for dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - toggle: - primary: black - accent: green - icon: material/toggle-switch-off-outline - name: Switch to light mode - # Palette toggle for light mode - - media: '(prefers-color-scheme: light)' - scheme: default - toggle: - primary: indigo - accent: green - icon: material/toggle-switch - name: Switch to system preference - icon: - repo: material/github - features: - - navigation.instant - - navigation.instant.progress - - navigation.indexes - - navigation.top - - navigation.tracking - - navigation.expand - - search.suggest - - search.highlight - -repo_name: -{{ REPO_OWNER }}-/-{{ REPO_NAME }}- -repo_url: https://github.com/-{{ REPO_OWNER }}-/-{{ REPO_NAME }}- - -plugins: - - search - -markdown_extensions: - - toc: - permalink: true # Adds a link icon to headings - - attr_list - - admonition - - md_in_html - - pymdownx.details # Enables collapsible admonitions - -extra: - social: - - icon: fontawesome/brands/discord - link: https://discord.gg/jedJWCPAhD - name: -{{ REPO_OWNER }}- on Discord - - icon: fontawesome/brands/github - link: https://github.com/-{{ REPO_OWNER }}-/ - name: -{{ REPO_OWNER }}- on GitHub - consent: - title: Cookie consent - description: >- - We use cookies to recognize your repeated visits and preferences, as well - as to measure the effectiveness of our documentation and whether users - find what they're searching for. With your consent, you're helping us to - make our documentation better. - actions: - - accept - - reject diff --git a/.github/zensical.toml b/.github/zensical.toml new file mode 100644 index 0000000..650af9f --- /dev/null +++ b/.github/zensical.toml @@ -0,0 +1,69 @@ +[project] +site_name = "-{{ REPO_NAME }}-" +repo_name = "-{{ REPO_OWNER }}-/-{{ REPO_NAME }}-" +repo_url = "https://github.com/-{{ REPO_OWNER }}-/-{{ REPO_NAME }}-" + +[project.theme] +variant = "classic" +language = "en" +logo = "Assets/icon.png" +favicon = "Assets/icon.png" +features = [ + "navigation.instant", + "navigation.instant.progress", + "navigation.indexes", + "navigation.top", + "navigation.tracking", + "navigation.expand", + "search.suggest", + "search.highlight", + "content.code.copy" +] + +[[project.theme.palette]] +media = "(prefers-color-scheme)" +toggle.icon = "lucide/sun-moon" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +primary = "black" +accent = "green" +toggle.icon = "lucide/moon" +toggle.name = "Switch to light mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +primary = "indigo" +accent = "green" +toggle.icon = "lucide/sun" +toggle.name = "Switch to system preference" + +[project.theme.icon] +repo = "fontawesome/brands/github" + +[project.markdown_extensions.toc] +permalink = true + +[project.markdown_extensions.attr_list] +[project.markdown_extensions.admonition] +[project.markdown_extensions.md_in_html] +[project.markdown_extensions.pymdownx.details] +[project.markdown_extensions.pymdownx.superfences] + +[[project.extra.social]] +icon = "fontawesome/brands/discord" +link = "https://discord.gg/jedJWCPAhD" +name = "-{{ REPO_OWNER }}- on Discord" + +[[project.extra.social]] +icon = "fontawesome/brands/github" +link = "https://github.com/-{{ REPO_OWNER }}-/" +name = "-{{ REPO_OWNER }}- on GitHub" + +[project.extra.consent] +title = "Cookie consent" +description = "We use cookies to recognize your repeated visits and preferences, as well as to measure the effectiveness of our documentation and whether users find what they're searching for. With your consent, you're helping us to make our documentation better." +actions = ["accept", "reject"] From ec2bcca00f633feb5bc2e7494486428e26256cf3 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 01:18:54 +0200 Subject: [PATCH 46/49] Add PR template foldable end sections Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/pull_request_template.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..6c93943 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,29 @@ + + +## New: + + + +## Changed: + + + +## Fixed: + + + +--- + +
+Technical details + + + +
+ +
+Relevant issues (or links) + + + +
From bc027141b7a164169d20159676f6a63b02e2487e Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 01:28:49 +0200 Subject: [PATCH 47/49] Remove pull request template Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/pull_request_template.md | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 6c93943..0000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,29 +0,0 @@ - - -## New: - - - -## Changed: - - - -## Fixed: - - - ---- - -
-Technical details - - - -
- -
-Relevant issues (or links) - - - -
From fe18c35bb4097842743693b8f24922d24f8cb6f8 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 01:45:59 +0200 Subject: [PATCH 48/49] Group command tests by feature file Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...neshopAccount.Tests.ps1 => Auth.Tests.ps1} | 57 +++++++ tests/Auth/Get-DomeneshopContext.Tests.ps1 | 69 --------- ...omeneshopDdns.Tests.ps1 => Ddns.Tests.ps1} | 0 tests/Dns/Add-DomeneshopDnsRecord.Tests.ps1 | 40 ----- tests/Dns/Dns.Tests.ps1 | 131 ++++++++++++++++ tests/Dns/Get-DomeneshopDnsRecord.Tests.ps1 | 49 ------ .../Dns/Remove-DomeneshopDnsRecord.Tests.ps1 | 38 ----- tests/Dns/Set-DomeneshopDnsRecord.Tests.ps1 | 40 ----- ...shopDomain.Tests.ps1 => Domains.Tests.ps1} | 0 .../Forwards/Add-DomeneshopForward.Tests.ps1 | 40 ----- tests/Forwards/Forwards.Tests.ps1 | 143 ++++++++++++++++++ .../Forwards/Get-DomeneshopForward.Tests.ps1 | 47 ------ .../Remove-DomeneshopForward.Tests.ps1 | 44 ------ .../Forwards/Set-DomeneshopForward.Tests.ps1 | 48 ------ ...opInvoice.Tests.ps1 => Invoices.Tests.ps1} | 0 tests/TestLayout.Tests.ps1 | 27 +++- 16 files changed, 351 insertions(+), 422 deletions(-) rename tests/Auth/{Connect-DomeneshopAccount.Tests.ps1 => Auth.Tests.ps1} (73%) delete mode 100644 tests/Auth/Get-DomeneshopContext.Tests.ps1 rename tests/Ddns/{Update-DomeneshopDdns.Tests.ps1 => Ddns.Tests.ps1} (100%) delete mode 100644 tests/Dns/Add-DomeneshopDnsRecord.Tests.ps1 create mode 100644 tests/Dns/Dns.Tests.ps1 delete mode 100644 tests/Dns/Get-DomeneshopDnsRecord.Tests.ps1 delete mode 100644 tests/Dns/Remove-DomeneshopDnsRecord.Tests.ps1 delete mode 100644 tests/Dns/Set-DomeneshopDnsRecord.Tests.ps1 rename tests/Domains/{Get-DomeneshopDomain.Tests.ps1 => Domains.Tests.ps1} (100%) delete mode 100644 tests/Forwards/Add-DomeneshopForward.Tests.ps1 create mode 100644 tests/Forwards/Forwards.Tests.ps1 delete mode 100644 tests/Forwards/Get-DomeneshopForward.Tests.ps1 delete mode 100644 tests/Forwards/Remove-DomeneshopForward.Tests.ps1 delete mode 100644 tests/Forwards/Set-DomeneshopForward.Tests.ps1 rename tests/Invoices/{Get-DomeneshopInvoice.Tests.ps1 => Invoices.Tests.ps1} (100%) diff --git a/tests/Auth/Connect-DomeneshopAccount.Tests.ps1 b/tests/Auth/Auth.Tests.ps1 similarity index 73% rename from tests/Auth/Connect-DomeneshopAccount.Tests.ps1 rename to tests/Auth/Auth.Tests.ps1 index c253034..258b0df 100644 --- a/tests/Auth/Connect-DomeneshopAccount.Tests.ps1 +++ b/tests/Auth/Auth.Tests.ps1 @@ -133,3 +133,60 @@ Describe 'Connect-DomeneshopAccount' { Should -Invoke Set-Context -Times 0 -Exactly } } + +Describe 'Get-DomeneshopContext' { + BeforeAll { + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') + } + + It 'gets the configured default context' { + Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = 'demo' } } + Mock Get-Context { [pscustomobject]@{ ID = 'demo' } } + + $result = Get-DomeneshopContext + + $result.ID | Should -Be 'demo' + Should -Invoke Get-Context -Times 1 -Exactly -ParameterFilter { + $ID -eq 'demo' -and $Vault -eq 'Domeneshop' + } + } + + It 'throws when no default context is configured' { + Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = '' } } + + { Get-DomeneshopContext } | Should -Throw '*No default Domeneshop context found*' + } + + It 'rejects an explicit request for the reserved module configuration context' { + { Get-DomeneshopContext -Context '__Domeneshop.Config' } | + Should -Throw '*reserved for Domeneshop module configuration*' + } + + It 'throws when the configured default context is missing from the vault' { + Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = 'missing' } } + Mock Get-Context {} + + { Get-DomeneshopContext } | + Should -Throw '*Domeneshop context*missing*was not found in the Domeneshop vault*' + } + + It 'throws when an explicitly requested context is missing from the vault' { + Mock Get-Context {} + + { Get-DomeneshopContext -Context 'missing' } | + Should -Throw '*Domeneshop context*missing*was not found in the Domeneshop vault*' + } + + It 'excludes the module configuration when listing contexts' { + Mock Get-Context { + @( + [pscustomobject]@{ ID = '__Domeneshop.Config' } + [pscustomobject]@{ ID = 'demo' } + ) + } + + $result = Get-DomeneshopContext -ListAvailable + + $result.ID | Should -Be 'demo' + } +} diff --git a/tests/Auth/Get-DomeneshopContext.Tests.ps1 b/tests/Auth/Get-DomeneshopContext.Tests.ps1 deleted file mode 100644 index d621529..0000000 --- a/tests/Auth/Get-DomeneshopContext.Tests.ps1 +++ /dev/null @@ -1,69 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Get-DomeneshopContext' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - It 'gets the configured default context' { - Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = 'demo' } } - Mock Get-Context { [pscustomobject]@{ ID = 'demo' } } - - $result = Get-DomeneshopContext - - $result.ID | Should -Be 'demo' - Should -Invoke Get-Context -Times 1 -Exactly -ParameterFilter { - $ID -eq 'demo' -and $Vault -eq 'Domeneshop' - } - } - - It 'throws when no default context is configured' { - Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = '' } } - - { Get-DomeneshopContext } | Should -Throw '*No default Domeneshop context found*' - } - - It 'rejects an explicit request for the reserved module configuration context' { - { Get-DomeneshopContext -Context '__Domeneshop.Config' } | - Should -Throw '*reserved for Domeneshop module configuration*' - } - - It 'throws when the configured default context is missing from the vault' { - Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = 'missing' } } - Mock Get-Context {} - - { Get-DomeneshopContext } | - Should -Throw '*Domeneshop context*missing*was not found in the Domeneshop vault*' - } - - It 'throws when an explicitly requested context is missing from the vault' { - Mock Get-Context {} - - { Get-DomeneshopContext -Context 'missing' } | - Should -Throw '*Domeneshop context*missing*was not found in the Domeneshop vault*' - } - - It 'excludes the module configuration when listing contexts' { - Mock Get-Context { - @( - [pscustomobject]@{ ID = '__Domeneshop.Config' } - [pscustomobject]@{ ID = 'demo' } - ) - } - - $result = Get-DomeneshopContext -ListAvailable - - $result.ID | Should -Be 'demo' - } -} diff --git a/tests/Ddns/Update-DomeneshopDdns.Tests.ps1 b/tests/Ddns/Ddns.Tests.ps1 similarity index 100% rename from tests/Ddns/Update-DomeneshopDdns.Tests.ps1 rename to tests/Ddns/Ddns.Tests.ps1 diff --git a/tests/Dns/Add-DomeneshopDnsRecord.Tests.ps1 b/tests/Dns/Add-DomeneshopDnsRecord.Tests.ps1 deleted file mode 100644 index 0d952a2..0000000 --- a/tests/Dns/Add-DomeneshopDnsRecord.Tests.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Add-DomeneshopDnsRecord' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - $script:Record = @{ host = 'www'; type = 'A'; data = '192.0.2.10' } - } - - It 'posts the DNS record to the domain endpoint' { - Add-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Record $script:Record - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Post' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns' -and - $Body -eq $script:Record - } - } - - It 'does not send a request when WhatIf is specified' { - Add-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Record $script:Record -WhatIf - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Dns/Dns.Tests.ps1 b/tests/Dns/Dns.Tests.ps1 new file mode 100644 index 0000000..eea9c3c --- /dev/null +++ b/tests/Dns/Dns.Tests.ps1 @@ -0,0 +1,131 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Add-DomeneshopDnsRecord' { + BeforeAll { + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + $script:Record = @{ host = 'www'; type = 'A'; data = '192.0.2.10' } + } + + It 'posts the DNS record to the domain endpoint' { + Add-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Record $script:Record + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Post' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns' -and + $Body -eq $script:Record + } + } + + It 'does not send a request when WhatIf is specified' { + Add-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Record $script:Record -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} + +Describe 'Get-DomeneshopDnsRecord' { + BeforeAll { + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'builds an escaped list query' { + Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordHost 'home office' -Type 'A' -Data '192.0.2.10' + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns?host=home%20office&type=A&data=192.0.2.10' + } + } + + It 'gets a DNS record by ID' { + Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' + } + } + + It 'rejects whitespace-only list filters' { + { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordHost ' ' } | Should -Throw + { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Type ' ' } | Should -Throw + { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Data ' ' } | Should -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} + +Describe 'Remove-DomeneshopDnsRecord' { + BeforeAll { + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'deletes the DNS record endpoint' { + Remove-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Confirm:$false + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Delete' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' + } + } + + It 'does not send a request when WhatIf is specified' { + Remove-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} + +Describe 'Set-DomeneshopDnsRecord' { + BeforeAll { + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + $script:Record = @{ host = 'www'; type = 'A'; data = '192.0.2.20' } + } + + It 'puts the replacement record to the record endpoint' { + Set-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Record $script:Record + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Put' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' -and + $Body -eq $script:Record + } + } + + It 'does not send a request when WhatIf is specified' { + Set-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Record $script:Record -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} diff --git a/tests/Dns/Get-DomeneshopDnsRecord.Tests.ps1 b/tests/Dns/Get-DomeneshopDnsRecord.Tests.ps1 deleted file mode 100644 index b30384b..0000000 --- a/tests/Dns/Get-DomeneshopDnsRecord.Tests.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Get-DomeneshopDnsRecord' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - } - - It 'builds an escaped list query' { - Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordHost 'home office' -Type 'A' -Data '192.0.2.10' - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns?host=home%20office&type=A&data=192.0.2.10' - } - } - - It 'gets a DNS record by ID' { - Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' - } - } - - It 'rejects whitespace-only list filters' { - { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordHost ' ' } | Should -Throw - { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Type ' ' } | Should -Throw - { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Data ' ' } | Should -Throw - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Dns/Remove-DomeneshopDnsRecord.Tests.ps1 b/tests/Dns/Remove-DomeneshopDnsRecord.Tests.ps1 deleted file mode 100644 index 043fb6a..0000000 --- a/tests/Dns/Remove-DomeneshopDnsRecord.Tests.ps1 +++ /dev/null @@ -1,38 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Remove-DomeneshopDnsRecord' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - } - - It 'deletes the DNS record endpoint' { - Remove-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Confirm:$false - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Delete' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' - } - } - - It 'does not send a request when WhatIf is specified' { - Remove-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -WhatIf - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Dns/Set-DomeneshopDnsRecord.Tests.ps1 b/tests/Dns/Set-DomeneshopDnsRecord.Tests.ps1 deleted file mode 100644 index 86b7c0f..0000000 --- a/tests/Dns/Set-DomeneshopDnsRecord.Tests.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Set-DomeneshopDnsRecord' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - $script:Record = @{ host = 'www'; type = 'A'; data = '192.0.2.20' } - } - - It 'puts the replacement record to the record endpoint' { - Set-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Record $script:Record - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Put' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' -and - $Body -eq $script:Record - } - } - - It 'does not send a request when WhatIf is specified' { - Set-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Record $script:Record -WhatIf - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Domains/Get-DomeneshopDomain.Tests.ps1 b/tests/Domains/Domains.Tests.ps1 similarity index 100% rename from tests/Domains/Get-DomeneshopDomain.Tests.ps1 rename to tests/Domains/Domains.Tests.ps1 diff --git a/tests/Forwards/Add-DomeneshopForward.Tests.ps1 b/tests/Forwards/Add-DomeneshopForward.Tests.ps1 deleted file mode 100644 index 17ecfda..0000000 --- a/tests/Forwards/Add-DomeneshopForward.Tests.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Add-DomeneshopForward' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - $script:Forward = @{ host = 'www'; url = 'https://example.net' } - } - - It 'posts the forward to the domain endpoint' { - Add-DomeneshopForward -Context 'demo' -DomainID 42 -Forward $script:Forward - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Post' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/' -and - $Body -eq $script:Forward - } - } - - It 'does not send a request when WhatIf is specified' { - Add-DomeneshopForward -Context 'demo' -DomainID 42 -Forward $script:Forward -WhatIf - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Forwards/Forwards.Tests.ps1 b/tests/Forwards/Forwards.Tests.ps1 new file mode 100644 index 0000000..8738600 --- /dev/null +++ b/tests/Forwards/Forwards.Tests.ps1 @@ -0,0 +1,143 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Add-DomeneshopForward' { + BeforeAll { + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + $script:Forward = @{ host = 'www'; url = 'https://example.net' } + } + + It 'posts the forward to the domain endpoint' { + Add-DomeneshopForward -Context 'demo' -DomainID 42 -Forward $script:Forward + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Post' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/' -and + $Body -eq $script:Forward + } + } + + It 'does not send a request when WhatIf is specified' { + Add-DomeneshopForward -Context 'demo' -DomainID 42 -Forward $script:Forward -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} + +Describe 'Get-DomeneshopForward' { + BeforeAll { + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'lists forwards for a domain' { + Get-DomeneshopForward -Context 'demo' -DomainID 42 + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/' + } + } + + It 'gets a forward by escaped host name' { + Get-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'home office' + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/home%20office' + } + } + + It 'rejects whitespace-only forward hosts' { + { Get-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' } | Should -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} + +Describe 'Remove-DomeneshopForward' { + BeforeAll { + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'deletes the host endpoint' { + Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Confirm:$false + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Delete' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/www' + } + } + + It 'does not send a request when WhatIf is specified' { + Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } + + It 'rejects whitespace-only forward hosts' { + { Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' -Confirm:$false } | Should -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} + +Describe 'Set-DomeneshopForward' { + BeforeAll { + . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + $script:Forward = @{ host = 'www'; url = 'https://example.org' } + } + + It 'puts the replacement forward to the host endpoint' { + Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Forward $script:Forward + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Put' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/www' -and + $Body -eq $script:Forward + } + } + + It 'does not send a request when WhatIf is specified' { + Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Forward $script:Forward -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } + + It 'rejects whitespace-only forward hosts' { + { + Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' -Forward $script:Forward + } | Should -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} diff --git a/tests/Forwards/Get-DomeneshopForward.Tests.ps1 b/tests/Forwards/Get-DomeneshopForward.Tests.ps1 deleted file mode 100644 index dc233eb..0000000 --- a/tests/Forwards/Get-DomeneshopForward.Tests.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Get-DomeneshopForward' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - } - - It 'lists forwards for a domain' { - Get-DomeneshopForward -Context 'demo' -DomainID 42 - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/' - } - } - - It 'gets a forward by escaped host name' { - Get-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'home office' - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/home%20office' - } - } - - It 'rejects whitespace-only forward hosts' { - { Get-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' } | Should -Throw - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Forwards/Remove-DomeneshopForward.Tests.ps1 b/tests/Forwards/Remove-DomeneshopForward.Tests.ps1 deleted file mode 100644 index e5c9f73..0000000 --- a/tests/Forwards/Remove-DomeneshopForward.Tests.ps1 +++ /dev/null @@ -1,44 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Remove-DomeneshopForward' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - } - - It 'deletes the host endpoint' { - Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Confirm:$false - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Delete' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/www' - } - } - - It 'does not send a request when WhatIf is specified' { - Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -WhatIf - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } - - It 'rejects whitespace-only forward hosts' { - { Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' -Confirm:$false } | Should -Throw - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Forwards/Set-DomeneshopForward.Tests.ps1 b/tests/Forwards/Set-DomeneshopForward.Tests.ps1 deleted file mode 100644 index 5037b95..0000000 --- a/tests/Forwards/Set-DomeneshopForward.Tests.ps1 +++ /dev/null @@ -1,48 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Set-DomeneshopForward' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - $script:Forward = @{ host = 'www'; url = 'https://example.org' } - } - - It 'puts the replacement forward to the host endpoint' { - Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Forward $script:Forward - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Put' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/www' -and - $Body -eq $script:Forward - } - } - - It 'does not send a request when WhatIf is specified' { - Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Forward $script:Forward -WhatIf - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } - - It 'rejects whitespace-only forward hosts' { - { - Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' -Forward $script:Forward - } | Should -Throw - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Invoices/Get-DomeneshopInvoice.Tests.ps1 b/tests/Invoices/Invoices.Tests.ps1 similarity index 100% rename from tests/Invoices/Get-DomeneshopInvoice.Tests.ps1 rename to tests/Invoices/Invoices.Tests.ps1 diff --git a/tests/TestLayout.Tests.ps1 b/tests/TestLayout.Tests.ps1 index 6259d1b..ab213aa 100644 --- a/tests/TestLayout.Tests.ps1 +++ b/tests/TestLayout.Tests.ps1 @@ -8,16 +8,29 @@ $publicFunctionsPath = Join-Path -Path $sourcePath -ChildPath 'functions/public' $testCases = Get-ChildItem -Path $publicFunctionsPath -Filter '*.ps1' -Recurse | ForEach-Object { $relativePath = [IO.Path]::GetRelativePath($publicFunctionsPath, $_.FullName) - @{ - FunctionName = $_.BaseName - TestPath = Join-Path -Path $PSScriptRoot -ChildPath ( - [IO.Path]::ChangeExtension($relativePath, '.Tests.ps1') - ) + $relativeDirectory = Split-Path -Path $relativePath -Parent + if ([string]::IsNullOrWhiteSpace($relativeDirectory) -or $relativeDirectory -eq '.') { + @{ + TestScope = $_.BaseName + TestPath = Join-Path -Path $PSScriptRoot -ChildPath ( + [IO.Path]::ChangeExtension($relativePath, '.Tests.ps1') + ) + } + } else { + $groupName = Split-Path -Path $relativeDirectory -Leaf + @{ + TestScope = $groupName + TestPath = Join-Path -Path $PSScriptRoot -ChildPath ( + Join-Path -Path $groupName -ChildPath "$groupName.Tests.ps1" + ) + } } - } + } | + Group-Object -Property TestPath | + ForEach-Object { $_.Group[0] } Describe 'Public command test layout' { - It 'mirrors the public source path for ' -ForEach $testCases { + It 'has a grouped test file for ' -ForEach $testCases { Test-Path -LiteralPath $TestPath -PathType Leaf | Should -BeTrue } } From 272aca40f57294b7292131ecbe237f27b7e96fce Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 26 Jul 2026 02:11:09 +0200 Subject: [PATCH 49/49] Consolidate command tests by group Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...neshopAccount.Tests.ps1 => Auth.Tests.ps1} | 60 +++++++- tests/Auth/Get-DomeneshopContext.Tests.ps1 | 69 --------- ...omeneshopDdns.Tests.ps1 => Ddns.Tests.ps1} | 2 +- tests/Dns.Tests.ps1 | 132 ++++++++++++++++ tests/Dns/Add-DomeneshopDnsRecord.Tests.ps1 | 40 ----- tests/Dns/Get-DomeneshopDnsRecord.Tests.ps1 | 49 ------ .../Dns/Remove-DomeneshopDnsRecord.Tests.ps1 | 38 ----- tests/Dns/Set-DomeneshopDnsRecord.Tests.ps1 | 40 ----- ...shopDomain.Tests.ps1 => Domains.Tests.ps1} | 2 +- tests/Forwards.Tests.ps1 | 144 ++++++++++++++++++ .../Forwards/Add-DomeneshopForward.Tests.ps1 | 40 ----- .../Forwards/Get-DomeneshopForward.Tests.ps1 | 47 ------ .../Remove-DomeneshopForward.Tests.ps1 | 44 ------ .../Forwards/Set-DomeneshopForward.Tests.ps1 | 48 ------ ...opInvoice.Tests.ps1 => Invoices.Tests.ps1} | 2 +- tests/TestLayout.Tests.ps1 | 13 +- 16 files changed, 347 insertions(+), 423 deletions(-) rename tests/{Auth/Connect-DomeneshopAccount.Tests.ps1 => Auth.Tests.ps1} (72%) delete mode 100644 tests/Auth/Get-DomeneshopContext.Tests.ps1 rename tests/{Ddns/Update-DomeneshopDdns.Tests.ps1 => Ddns.Tests.ps1} (93%) create mode 100644 tests/Dns.Tests.ps1 delete mode 100644 tests/Dns/Add-DomeneshopDnsRecord.Tests.ps1 delete mode 100644 tests/Dns/Get-DomeneshopDnsRecord.Tests.ps1 delete mode 100644 tests/Dns/Remove-DomeneshopDnsRecord.Tests.ps1 delete mode 100644 tests/Dns/Set-DomeneshopDnsRecord.Tests.ps1 rename tests/{Domains/Get-DomeneshopDomain.Tests.ps1 => Domains.Tests.ps1} (93%) create mode 100644 tests/Forwards.Tests.ps1 delete mode 100644 tests/Forwards/Add-DomeneshopForward.Tests.ps1 delete mode 100644 tests/Forwards/Get-DomeneshopForward.Tests.ps1 delete mode 100644 tests/Forwards/Remove-DomeneshopForward.Tests.ps1 delete mode 100644 tests/Forwards/Set-DomeneshopForward.Tests.ps1 rename tests/{Invoices/Get-DomeneshopInvoice.Tests.ps1 => Invoices.Tests.ps1} (91%) diff --git a/tests/Auth/Connect-DomeneshopAccount.Tests.ps1 b/tests/Auth.Tests.ps1 similarity index 72% rename from tests/Auth/Connect-DomeneshopAccount.Tests.ps1 rename to tests/Auth.Tests.ps1 index c253034..769dc15 100644 --- a/tests/Auth/Connect-DomeneshopAccount.Tests.ps1 +++ b/tests/Auth.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Connect-DomeneshopAccount' { BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { @@ -133,3 +133,61 @@ Describe 'Connect-DomeneshopAccount' { Should -Invoke Set-Context -Times 0 -Exactly } } + +Describe 'Get-DomeneshopContext' { + BeforeAll { + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + } + + It 'gets the configured default context' { + Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = 'demo' } } + Mock Get-Context { [pscustomobject]@{ ID = 'demo' } } + + $result = Get-DomeneshopContext + + $result.ID | Should -Be 'demo' + Should -Invoke Get-Context -Times 1 -Exactly -ParameterFilter { + $ID -eq 'demo' -and $Vault -eq 'Domeneshop' + } + } + + It 'throws when no default context is configured' { + Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = '' } } + + { Get-DomeneshopContext } | Should -Throw '*No default Domeneshop context found*' + } + + It 'rejects an explicit request for the reserved module configuration context' { + { Get-DomeneshopContext -Context '__Domeneshop.Config' } | + Should -Throw '*reserved for Domeneshop module configuration*' + } + + It 'throws when the configured default context is missing from the vault' { + Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = 'missing' } } + Mock Get-Context {} + + { Get-DomeneshopContext } | + Should -Throw '*Domeneshop context*missing*was not found in the Domeneshop vault*' + } + + It 'throws when an explicitly requested context is missing from the vault' { + Mock Get-Context {} + + { Get-DomeneshopContext -Context 'missing' } | + Should -Throw '*Domeneshop context*missing*was not found in the Domeneshop vault*' + } + + It 'excludes the module configuration when listing contexts' { + Mock Get-Context { + @( + [pscustomobject]@{ ID = '__Domeneshop.Config' } + [pscustomobject]@{ ID = 'demo' } + ) + } + + $result = Get-DomeneshopContext -ListAvailable + + $result.ID | Should -Be 'demo' + } +} + diff --git a/tests/Auth/Get-DomeneshopContext.Tests.ps1 b/tests/Auth/Get-DomeneshopContext.Tests.ps1 deleted file mode 100644 index d621529..0000000 --- a/tests/Auth/Get-DomeneshopContext.Tests.ps1 +++ /dev/null @@ -1,69 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Get-DomeneshopContext' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - It 'gets the configured default context' { - Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = 'demo' } } - Mock Get-Context { [pscustomobject]@{ ID = 'demo' } } - - $result = Get-DomeneshopContext - - $result.ID | Should -Be 'demo' - Should -Invoke Get-Context -Times 1 -Exactly -ParameterFilter { - $ID -eq 'demo' -and $Vault -eq 'Domeneshop' - } - } - - It 'throws when no default context is configured' { - Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = '' } } - - { Get-DomeneshopContext } | Should -Throw '*No default Domeneshop context found*' - } - - It 'rejects an explicit request for the reserved module configuration context' { - { Get-DomeneshopContext -Context '__Domeneshop.Config' } | - Should -Throw '*reserved for Domeneshop module configuration*' - } - - It 'throws when the configured default context is missing from the vault' { - Mock Get-DomeneshopConfig { [pscustomobject]@{ DefaultContext = 'missing' } } - Mock Get-Context {} - - { Get-DomeneshopContext } | - Should -Throw '*Domeneshop context*missing*was not found in the Domeneshop vault*' - } - - It 'throws when an explicitly requested context is missing from the vault' { - Mock Get-Context {} - - { Get-DomeneshopContext -Context 'missing' } | - Should -Throw '*Domeneshop context*missing*was not found in the Domeneshop vault*' - } - - It 'excludes the module configuration when listing contexts' { - Mock Get-Context { - @( - [pscustomobject]@{ ID = '__Domeneshop.Config' } - [pscustomobject]@{ ID = 'demo' } - ) - } - - $result = Get-DomeneshopContext -ListAvailable - - $result.ID | Should -Be 'demo' - } -} diff --git a/tests/Ddns/Update-DomeneshopDdns.Tests.ps1 b/tests/Ddns.Tests.ps1 similarity index 93% rename from tests/Ddns/Update-DomeneshopDdns.Tests.ps1 rename to tests/Ddns.Tests.ps1 index 33bf9eb..597d42e 100644 --- a/tests/Ddns/Update-DomeneshopDdns.Tests.ps1 +++ b/tests/Ddns.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Update-DomeneshopDdns' { BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Dns.Tests.ps1 b/tests/Dns.Tests.ps1 new file mode 100644 index 0000000..18b7c4b --- /dev/null +++ b/tests/Dns.Tests.ps1 @@ -0,0 +1,132 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Add-DomeneshopDnsRecord' { + BeforeAll { + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + $script:Record = @{ host = 'www'; type = 'A'; data = '192.0.2.10' } + } + + It 'posts the DNS record to the domain endpoint' { + Add-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Record $script:Record + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Post' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns' -and + $Body -eq $script:Record + } + } + + It 'does not send a request when WhatIf is specified' { + Add-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Record $script:Record -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} + +Describe 'Get-DomeneshopDnsRecord' { + BeforeAll { + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'builds an escaped list query' { + Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordHost 'home office' -Type 'A' -Data '192.0.2.10' + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns?host=home%20office&type=A&data=192.0.2.10' + } + } + + It 'gets a DNS record by ID' { + Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' + } + } + + It 'rejects whitespace-only list filters' { + { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordHost ' ' } | Should -Throw + { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Type ' ' } | Should -Throw + { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Data ' ' } | Should -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} + +Describe 'Remove-DomeneshopDnsRecord' { + BeforeAll { + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'deletes the DNS record endpoint' { + Remove-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Confirm:$false + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Delete' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' + } + } + + It 'does not send a request when WhatIf is specified' { + Remove-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} + +Describe 'Set-DomeneshopDnsRecord' { + BeforeAll { + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + $script:Record = @{ host = 'www'; type = 'A'; data = '192.0.2.20' } + } + + It 'puts the replacement record to the record endpoint' { + Set-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Record $script:Record + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Put' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' -and + $Body -eq $script:Record + } + } + + It 'does not send a request when WhatIf is specified' { + Set-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Record $script:Record -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} + diff --git a/tests/Dns/Add-DomeneshopDnsRecord.Tests.ps1 b/tests/Dns/Add-DomeneshopDnsRecord.Tests.ps1 deleted file mode 100644 index 0d952a2..0000000 --- a/tests/Dns/Add-DomeneshopDnsRecord.Tests.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Add-DomeneshopDnsRecord' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - $script:Record = @{ host = 'www'; type = 'A'; data = '192.0.2.10' } - } - - It 'posts the DNS record to the domain endpoint' { - Add-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Record $script:Record - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Post' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns' -and - $Body -eq $script:Record - } - } - - It 'does not send a request when WhatIf is specified' { - Add-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Record $script:Record -WhatIf - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Dns/Get-DomeneshopDnsRecord.Tests.ps1 b/tests/Dns/Get-DomeneshopDnsRecord.Tests.ps1 deleted file mode 100644 index b30384b..0000000 --- a/tests/Dns/Get-DomeneshopDnsRecord.Tests.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Get-DomeneshopDnsRecord' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - } - - It 'builds an escaped list query' { - Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordHost 'home office' -Type 'A' -Data '192.0.2.10' - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns?host=home%20office&type=A&data=192.0.2.10' - } - } - - It 'gets a DNS record by ID' { - Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' - } - } - - It 'rejects whitespace-only list filters' { - { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordHost ' ' } | Should -Throw - { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Type ' ' } | Should -Throw - { Get-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -Data ' ' } | Should -Throw - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Dns/Remove-DomeneshopDnsRecord.Tests.ps1 b/tests/Dns/Remove-DomeneshopDnsRecord.Tests.ps1 deleted file mode 100644 index 043fb6a..0000000 --- a/tests/Dns/Remove-DomeneshopDnsRecord.Tests.ps1 +++ /dev/null @@ -1,38 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Remove-DomeneshopDnsRecord' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - } - - It 'deletes the DNS record endpoint' { - Remove-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Confirm:$false - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Delete' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' - } - } - - It 'does not send a request when WhatIf is specified' { - Remove-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -WhatIf - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Dns/Set-DomeneshopDnsRecord.Tests.ps1 b/tests/Dns/Set-DomeneshopDnsRecord.Tests.ps1 deleted file mode 100644 index 86b7c0f..0000000 --- a/tests/Dns/Set-DomeneshopDnsRecord.Tests.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Set-DomeneshopDnsRecord' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - $script:Record = @{ host = 'www'; type = 'A'; data = '192.0.2.20' } - } - - It 'puts the replacement record to the record endpoint' { - Set-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Record $script:Record - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Put' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/dns/7' -and - $Body -eq $script:Record - } - } - - It 'does not send a request when WhatIf is specified' { - Set-DomeneshopDnsRecord -Context 'demo' -DomainID 42 -RecordID 7 -Record $script:Record -WhatIf - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Domains/Get-DomeneshopDomain.Tests.ps1 b/tests/Domains.Tests.ps1 similarity index 93% rename from tests/Domains/Get-DomeneshopDomain.Tests.ps1 rename to tests/Domains.Tests.ps1 index a8d2a1b..deb77f4 100644 --- a/tests/Domains/Get-DomeneshopDomain.Tests.ps1 +++ b/tests/Domains.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Get-DomeneshopDomain' { BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/Forwards.Tests.ps1 b/tests/Forwards.Tests.ps1 new file mode 100644 index 0000000..815151a --- /dev/null +++ b/tests/Forwards.Tests.ps1 @@ -0,0 +1,144 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester mock parameter filters.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester test setup.' +)] +[CmdletBinding()] +param() + +Describe 'Add-DomeneshopForward' { + BeforeAll { + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + $script:Forward = @{ host = 'www'; url = 'https://example.net' } + } + + It 'posts the forward to the domain endpoint' { + Add-DomeneshopForward -Context 'demo' -DomainID 42 -Forward $script:Forward + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Post' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/' -and + $Body -eq $script:Forward + } + } + + It 'does not send a request when WhatIf is specified' { + Add-DomeneshopForward -Context 'demo' -DomainID 42 -Forward $script:Forward -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} + +Describe 'Get-DomeneshopForward' { + BeforeAll { + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'lists forwards for a domain' { + Get-DomeneshopForward -Context 'demo' -DomainID 42 + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/' + } + } + + It 'gets a forward by escaped host name' { + Get-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'home office' + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/home%20office' + } + } + + It 'rejects whitespace-only forward hosts' { + { Get-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' } | Should -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} + +Describe 'Remove-DomeneshopForward' { + BeforeAll { + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + } + + It 'deletes the host endpoint' { + Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Confirm:$false + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Delete' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/www' + } + } + + It 'does not send a request when WhatIf is specified' { + Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } + + It 'rejects whitespace-only forward hosts' { + { Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' -Confirm:$false } | Should -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} + +Describe 'Set-DomeneshopForward' { + BeforeAll { + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') + } + + BeforeEach { + Mock Get-DomeneshopContext { $script:DomeneshopTestContext } + Mock Invoke-DomeneshopApiRequest {} + $script:Forward = @{ host = 'www'; url = 'https://example.org' } + } + + It 'puts the replacement forward to the host endpoint' { + Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Forward $script:Forward + + Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { + $Method -eq 'Put' -and + $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/www' -and + $Body -eq $script:Forward + } + } + + It 'does not send a request when WhatIf is specified' { + Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Forward $script:Forward -WhatIf + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } + + It 'rejects whitespace-only forward hosts' { + { + Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' -Forward $script:Forward + } | Should -Throw + + Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly + } +} + diff --git a/tests/Forwards/Add-DomeneshopForward.Tests.ps1 b/tests/Forwards/Add-DomeneshopForward.Tests.ps1 deleted file mode 100644 index 17ecfda..0000000 --- a/tests/Forwards/Add-DomeneshopForward.Tests.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Add-DomeneshopForward' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - $script:Forward = @{ host = 'www'; url = 'https://example.net' } - } - - It 'posts the forward to the domain endpoint' { - Add-DomeneshopForward -Context 'demo' -DomainID 42 -Forward $script:Forward - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Post' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/' -and - $Body -eq $script:Forward - } - } - - It 'does not send a request when WhatIf is specified' { - Add-DomeneshopForward -Context 'demo' -DomainID 42 -Forward $script:Forward -WhatIf - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Forwards/Get-DomeneshopForward.Tests.ps1 b/tests/Forwards/Get-DomeneshopForward.Tests.ps1 deleted file mode 100644 index dc233eb..0000000 --- a/tests/Forwards/Get-DomeneshopForward.Tests.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Get-DomeneshopForward' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - } - - It 'lists forwards for a domain' { - Get-DomeneshopForward -Context 'demo' -DomainID 42 - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/' - } - } - - It 'gets a forward by escaped host name' { - Get-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'home office' - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Get' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/home%20office' - } - } - - It 'rejects whitespace-only forward hosts' { - { Get-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' } | Should -Throw - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Forwards/Remove-DomeneshopForward.Tests.ps1 b/tests/Forwards/Remove-DomeneshopForward.Tests.ps1 deleted file mode 100644 index e5c9f73..0000000 --- a/tests/Forwards/Remove-DomeneshopForward.Tests.ps1 +++ /dev/null @@ -1,44 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Remove-DomeneshopForward' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - } - - It 'deletes the host endpoint' { - Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Confirm:$false - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Delete' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/www' - } - } - - It 'does not send a request when WhatIf is specified' { - Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -WhatIf - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } - - It 'rejects whitespace-only forward hosts' { - { Remove-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' -Confirm:$false } | Should -Throw - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Forwards/Set-DomeneshopForward.Tests.ps1 b/tests/Forwards/Set-DomeneshopForward.Tests.ps1 deleted file mode 100644 index 5037b95..0000000 --- a/tests/Forwards/Set-DomeneshopForward.Tests.ps1 +++ /dev/null @@ -1,48 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } - -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester mock parameter filters.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester test setup.' -)] -[CmdletBinding()] -param() - -Describe 'Set-DomeneshopForward' { - BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') - } - - BeforeEach { - Mock Get-DomeneshopContext { $script:DomeneshopTestContext } - Mock Invoke-DomeneshopApiRequest {} - $script:Forward = @{ host = 'www'; url = 'https://example.org' } - } - - It 'puts the replacement forward to the host endpoint' { - Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Forward $script:Forward - - Should -Invoke Invoke-DomeneshopApiRequest -Times 1 -Exactly -ParameterFilter { - $Method -eq 'Put' -and - $Uri -eq 'https://api.domeneshop.no/v0/domains/42/forwards/www' -and - $Body -eq $script:Forward - } - } - - It 'does not send a request when WhatIf is specified' { - Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost 'www' -Forward $script:Forward -WhatIf - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } - - It 'rejects whitespace-only forward hosts' { - { - Set-DomeneshopForward -Context 'demo' -DomainID 42 -ForwardHost ' ' -Forward $script:Forward - } | Should -Throw - - Should -Invoke Invoke-DomeneshopApiRequest -Times 0 -Exactly - } -} diff --git a/tests/Invoices/Get-DomeneshopInvoice.Tests.ps1 b/tests/Invoices.Tests.ps1 similarity index 91% rename from tests/Invoices/Get-DomeneshopInvoice.Tests.ps1 rename to tests/Invoices.Tests.ps1 index 19bebd9..e879b5c 100644 --- a/tests/Invoices/Get-DomeneshopInvoice.Tests.ps1 +++ b/tests/Invoices.Tests.ps1 @@ -13,7 +13,7 @@ param() Describe 'Get-DomeneshopInvoice' { BeforeAll { - . (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Domeneshop.TestSetup.ps1') + . (Join-Path -Path $PSScriptRoot -ChildPath 'Domeneshop.TestSetup.ps1') } BeforeEach { diff --git a/tests/TestLayout.Tests.ps1 b/tests/TestLayout.Tests.ps1 index 6259d1b..c88b6b8 100644 --- a/tests/TestLayout.Tests.ps1 +++ b/tests/TestLayout.Tests.ps1 @@ -8,16 +8,21 @@ $publicFunctionsPath = Join-Path -Path $sourcePath -ChildPath 'functions/public' $testCases = Get-ChildItem -Path $publicFunctionsPath -Filter '*.ps1' -Recurse | ForEach-Object { $relativePath = [IO.Path]::GetRelativePath($publicFunctionsPath, $_.FullName) + $group = Split-Path -Path $relativePath -Parent + $testFile = if ($group) { + "$group.Tests.ps1" + } else { + "$($_.BaseName).Tests.ps1" + } @{ FunctionName = $_.BaseName - TestPath = Join-Path -Path $PSScriptRoot -ChildPath ( - [IO.Path]::ChangeExtension($relativePath, '.Tests.ps1') - ) + TestFile = $testFile + TestPath = Join-Path -Path $PSScriptRoot -ChildPath $testFile } } Describe 'Public command test layout' { - It 'mirrors the public source path for ' -ForEach $testCases { + It 'covers in ' -ForEach $testCases { Test-Path -LiteralPath $TestPath -PathType Leaf | Should -BeTrue } }