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/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/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/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 new file mode 100644 index 0000000..be1ba64 --- /dev/null +++ b/src/functions/public/Domains/Get-DomeneshopDomain.ps1 @@ -0,0 +1,44 @@ +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(DefaultParameterSetName = 'List')] + param( + [Parameter(ParameterSetName = 'List')] + [string] $Domain, + + [Parameter(Mandatory, ParameterSetName = 'GetByID')] + [int] $DomainID, + + [Parameter()] + [string] $Context + ) + + $resolvedContext = Resolve-DomeneshopContext -Context $Context + $apiBaseUri = Get-DomeneshopApiBaseUri -Context $resolvedContext + + $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/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/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 +} diff --git a/tests/DomeneshopContext.Tests.ps1 b/tests/DomeneshopContext.Tests.ps1 new file mode 100644 index 0000000..b42e7b2 --- /dev/null +++ b/tests/DomeneshopContext.Tests.ps1 @@ -0,0 +1,236 @@ +[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' + } + } + + 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' + } + } +} 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!' }