Tests/InvokeHubspot.Tests.ps1
|
$moduleRoot = Split-Path -Parent $PSScriptRoot $manifestPath = Join-Path $moduleRoot 'InvokeHubspot.psd1' Describe 'InvokeHubspot module smoke tests' { It 'loads the module manifest' { $manifest = Test-ModuleManifest $manifestPath $manifest.Name | Should Be 'InvokeHubspot' $manifest.Version.ToString() | Should Be '0.1.3' } It 'imports the module and exposes the expected public commands' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop $commandNames = @(Get-Command -Module InvokeHubspot | Select-Object -ExpandProperty Name) ($commandNames -contains 'Get-HubspotConfiguration') | Should Be $true ($commandNames -contains 'Set-HubspotConfiguration') | Should Be $true ($commandNames -contains 'Set-HubspotAccessToken') | Should Be $true ($commandNames -contains 'Invoke-HubspotRest') | Should Be $true ($commandNames -contains 'Get-HubspotDeal') | Should Be $true ($commandNames -contains 'Get-HubspotDealDetails') | Should Be $true ($commandNames -contains 'Get-HubspotDeals') | Should Be $true ($commandNames -contains 'Search-HubspotDeals') | Should Be $true ($commandNames -contains 'New-HubspotDeal') | Should Be $true ($commandNames -contains 'Set-HubspotDeal') | Should Be $true ($commandNames -contains 'Remove-HubspotDeal') | Should Be $true } It 'updates configuration in-memory' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop $updatedConfiguration = Set-HubspotConfiguration -ServiceUserName 'TEST_HUBSPOT' -BaseUri 'https://api.hubapi.com/' -DealsPath '/crm/v3/objects/deals/' -DefaultPageSize 200 -DefaultRetryCount 5 -DefaultThrottleDelaySeconds 0.5 $updatedConfiguration.ServiceUserName | Should Be 'TEST_HUBSPOT' $updatedConfiguration.BaseUri | Should Be 'https://api.hubapi.com' $updatedConfiguration.DealsPath | Should Be 'crm/v3/objects/deals' $updatedConfiguration.DefaultPageSize | Should Be 200 $updatedConfiguration.DefaultRetryCount | Should Be 5 $updatedConfiguration.DefaultThrottleDelaySeconds | Should Be 0.5 } It 'converts HubSpot records into typed flat objects' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:HubspotPropertyDefinitionsByObjectType = @{ deals = @{ dealname = [pscustomobject]@{ name = 'dealname'; type = 'string' } amount = [pscustomobject]@{ name = 'amount'; type = 'number' } closedate = [pscustomobject]@{ name = 'closedate'; type = 'date' } is_closed = [pscustomobject]@{ name = 'is_closed'; type = 'bool' } stage = [pscustomobject]@{ name = 'stage'; type = 'enumeration'; fieldType = 'checkbox' } custom_json = [pscustomobject]@{ name = 'custom_json'; type = 'json' } } } $record = [pscustomobject]@{ id = '123' archived = $false createdAt = '2026-08-06T10:11:12Z' updatedAt = '2026-08-06T10:11:13Z' properties = [pscustomobject]@{ dealname = 'Test Deal' amount = '42.5' closedate = '2026-08-06' is_closed = 'true' stage = 'A;B' custom_json = '{"x":1}' } } $converted = Convert-HubspotRecordToObject -Record $record -ObjectType 'deals' $converted.Id | Should Be '123' $converted.Dealname | Should Be 'Test Deal' $converted.Amount | Should Be 42.5 $converted.is_closed | Should Be $true $converted.stage -join ',' | Should Be 'A,B' $converted.custom_json.x | Should Be 1 ([datetime]$converted.closedate).ToString('yyyy-MM-dd') | Should Be '2026-08-06' } } It 'translates dealstage ids to labels when pipeline metadata is available' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:HubspotPropertyDefinitionsByObjectType = @{ deals = @{ pipeline = [pscustomobject]@{ name = 'pipeline'; type = 'string' } dealstage = [pscustomobject]@{ name = 'dealstage'; type = 'string' } } } $script:HubspotDealPipelineDefinitionsById = @{ default = [pscustomobject]@{ Id = 'default' Label = 'Default Pipeline' Stages = @{ contractsent = [pscustomobject]@{ Id = 'contractsent' Label = 'Anfrage zugeordnet (hhpberlin Sales)' PipelineId = 'default' PipelineLabel = 'Default Pipeline' } } } } $script:HubspotDealPipelineDefinitionsLoaded = $true $record = [pscustomobject]@{ id = '456' archived = $false properties = [pscustomobject]@{ pipeline = 'default' dealstage = 'contractsent' } } $converted = Convert-HubspotRecordToObject -Record $record -ObjectType 'deals' $converted.dealstage | Should Be 'contractsent' $converted.dealstageLabel | Should Be 'Anfrage zugeordnet (hhpberlin Sales)' ($converted.PSObject.Properties.Name -contains 'dealstageId') | Should Be $false $converted.pipeline | Should Be 'default' $converted.pipelineLabel | Should Be 'Default Pipeline' } } It 'converts translated dealstage values back to internal ids before writing' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:HubspotDealPipelineDefinitionsById = @{ default = [pscustomobject]@{ Id = 'default' Label = 'Default Pipeline' Stages = @{ contractsent = [pscustomobject]@{ Id = 'contractsent' Label = 'Anfrage zugeordnet (hhpberlin Sales)' PipelineId = 'default' PipelineLabel = 'Default Pipeline' } } } } $script:HubspotDealPipelineDefinitionsLoaded = $true $properties = @{ pipeline = 'default' dealstageId = 'contractsent' dealstageLabel = 'Anfrage zugeordnet (hhpberlin Sales)' } $writeProperties = Convert-HubspotDealPropertiesForWrite -Properties $properties $writeProperties.dealstage | Should Be 'contractsent' $writeProperties.pipeline | Should Be 'default' $writeProperties.Contains('dealstageLabel') | Should Be $false } } It 'returns GUI-like deal details with display labels' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:HubspotPropertyDefinitionsByObjectType = @{ deals = @{ pipeline = [pscustomobject]@{ name = 'pipeline'; label = 'Pipeline'; type = 'enumeration'; fieldType = 'select'; groupName = 'dealinformation'; options = @([pscustomobject]@{ value = 'default'; label = 'hhpberlin Sales' }) } dealstage = [pscustomobject]@{ name = 'dealstage'; label = 'Deal-Phase'; type = 'enumeration'; fieldType = 'select'; groupName = 'dealinformation' } nda_safe = [pscustomobject]@{ name = 'nda_safe'; label = 'NDA / SAFE'; type = 'enumeration'; fieldType = 'radio'; groupName = 'dealinformation'; options = @([pscustomobject]@{ value = 'false'; label = 'Nein' }, [pscustomobject]@{ value = 'true'; label = 'Ja' }) } hubspot_owner_id = [pscustomobject]@{ name = 'hubspot_owner_id'; label = 'Fuer Deal zustaendiger Mitarbeiter'; type = 'number'; fieldType = 'number'; groupName = 'dealinformation' } } } $script:HubspotDealPipelineDefinitionsById = @{ default = [pscustomobject]@{ Id = 'default' Label = 'hhpberlin Sales' Stages = @{ '1110595160' = [pscustomobject]@{ Id = '1110595160' Label = 'Anfrage zugeordnet' PipelineId = 'default' PipelineLabel = 'hhpberlin Sales' } } } } $script:HubspotDealPipelineDefinitionsLoaded = $true function Invoke-HubspotRest { param ( [Parameter(Mandatory = $false)][string]$Path, [Parameter(Mandatory = $false)][hashtable]$Query, [Parameter(Mandatory = $false)][string]$AccessToken ) return [pscustomobject]@{ id = '63433921854' archived = $true createdAt = '2026-08-06T03:28:31Z' updatedAt = '2026-08-06T03:28:35Z' properties = [pscustomobject]@{ pipeline = 'default' dealstage = '1110595160' nda_safe = 'false' hubspot_owner_id = '41629779' } } } function Invoke-HubspotRequest { param ( [Parameter(Mandatory = $false)][string]$ResourcePath, [Parameter(Mandatory = $false)][string]$AccessToken ) if ($ResourcePath -eq 'crm/v3/owners/41629779') { return [pscustomobject]@{ id = '41629779' firstName = 'Jonny' lastName = 'Dunger' email = 'jonny.dunger@example.org' } } throw "Unexpected resource path: $ResourcePath" } $details = Get-HubspotDealDetails -Id '63433921854' -Properties @('pipeline', 'dealstage', 'nda_safe', 'hubspot_owner_id') ($details | Measure-Object).Count | Should Be 4 $stageDetail = $details | Where-Object { $_.PropertyName -eq 'dealstage' } $stageDetail.Label | Should Be 'Deal-Phase' $stageDetail.Value | Should Be '1110595160' $stageDetail.DisplayValue | Should Be 'Anfrage zugeordnet' $ndaDetail = $details | Where-Object { $_.PropertyName -eq 'nda_safe' } $ndaDetail.Label | Should Be 'NDA / SAFE' $ndaDetail.Value | Should Be 'false' $ndaDetail.DisplayValue | Should Be 'Nein' $ownerDetail = $details | Where-Object { $_.PropertyName -eq 'hubspot_owner_id' } $ownerDetail.Value | Should Be '41629779' $ownerDetail.DisplayValue | Should Be 'Jonny Dunger' } } It 'retries details read with archived=true on 404' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:HubspotPropertyDefinitionsByObjectType = @{ deals = @{ pipeline = [pscustomobject]@{ name = 'pipeline'; label = 'Pipeline'; type = 'string'; fieldType = 'text'; groupName = 'dealinformation' } } } $script:HubspotDetailsRetryAttempt = 0 function Invoke-HubspotRest { param ( [Parameter(Mandatory = $false)][string]$Path, [Parameter(Mandatory = $false)][hashtable]$Query, [Parameter(Mandatory = $false)][string]$AccessToken ) $script:HubspotDetailsRetryAttempt++ if (-not $Query.ContainsKey('archived')) { $ex = [System.Exception]::new('Not Found') Add-Member -InputObject $ex -MemberType NoteProperty -Name Response -Value ([pscustomobject]@{ StatusCode = 404 }) -Force throw $ex } return [pscustomobject]@{ id = '63433921854' archived = $true properties = [pscustomobject]@{ pipeline = 'default' } } } $details = Get-HubspotDealDetails -Id '63433921854' -Properties @('pipeline') ($details | Measure-Object).Count | Should Be 1 $details[0].PropertyName | Should Be 'pipeline' $script:HubspotDetailsRetryAttempt | Should Be 2 } } It 'keeps explicitly requested empty fields in details output' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:HubspotPropertyDefinitionsByObjectType = @{ deals = @{ nda_safe = [pscustomobject]@{ name = 'nda_safe'; label = 'NDA / SAFE'; type = 'enumeration'; fieldType = 'radio'; groupName = 'dealinformation' } } } function Invoke-HubspotRest { param ( [Parameter(Mandatory = $false)][string]$Path, [Parameter(Mandatory = $false)][hashtable]$Query, [Parameter(Mandatory = $false)][string]$AccessToken ) return [pscustomobject]@{ id = '1' archived = $false properties = [pscustomobject]@{ nda_safe = $null } } } $details = Get-HubspotDealDetails -Id '1' -Properties @('nda_safe') ($details | Measure-Object).Count | Should Be 1 $details[0].PropertyName | Should Be 'nda_safe' $details[0].Value | Should Be $null } } It 'uses HubSpot standard attributes by default and requests all only with -AllProperties' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:HubspotPropertyDefinitionsByObjectType = @{ deals = @{ dealname = [pscustomobject]@{ name = 'dealname'; type = 'string' } dealstage = [pscustomobject]@{ name = 'dealstage'; type = 'string' } pipeline = [pscustomobject]@{ name = 'pipeline'; type = 'string' } } } $script:CapturedGetDealQuery = $null function Invoke-HubspotRest { param ( [Parameter(Mandatory = $false)][string]$Path, [Parameter(Mandatory = $false)][hashtable]$Query, [Parameter(Mandatory = $false)][string]$AccessToken ) $script:CapturedGetDealQuery = $Query return [pscustomobject]@{ id = '123' archived = $false properties = [pscustomobject]@{ dealname = 'X' dealstage = 'closedlost' pipeline = 'default' } } } $null = Get-HubspotDeal -Id '123' $script:CapturedGetDealQuery.ContainsKey('properties') | Should Be $false $null = Get-HubspotDeal -Id '123' -AllProperties $script:CapturedGetDealQuery.ContainsKey('properties') | Should Be $true (($script:CapturedGetDealQuery.properties | Sort-Object) -join ',') | Should Be 'dealname,dealstage,pipeline' } } It 're-prompts token and retries once on invalid authentication' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:TokenPromptCalls = 0 $script:InvokeRestCalls = 0 function Get-HubspotBearerToken { param ( [Parameter(Mandatory = $false)][string]$AccessToken, [Parameter(Mandatory = $false)][switch]$ForcePrompt ) if ($ForcePrompt.IsPresent) { $script:TokenPromptCalls++ return 'new-token' } return 'old-token' } function Invoke-RestMethod { param ( [Parameter(Mandatory = $false)][uri]$Uri, [Parameter(Mandatory = $false)][string]$Method, [Parameter(Mandatory = $false)][hashtable]$Headers ) $script:InvokeRestCalls++ if ($Headers.Authorization -eq 'Bearer old-token') { $ex = [System.Exception]::new('Authentication credentials not found') Add-Member -InputObject $ex -MemberType NoteProperty -Name Response -Value ([pscustomobject]@{ StatusCode = 401 }) -Force $errorRecord = [System.Management.Automation.ErrorRecord]::new($ex, 'INVALID_AUTHENTICATION', [System.Management.Automation.ErrorCategory]::AuthenticationError, $null) $errorRecord.ErrorDetails = [System.Management.Automation.ErrorDetails]::new('{"status":"error","category":"INVALID_AUTHENTICATION"}') throw $errorRecord } return [pscustomobject]@{ results = @() } } $null = Invoke-HubspotRequest -ResourcePath 'crm/v3/objects/deals' $script:TokenPromptCalls | Should Be 1 $script:InvokeRestCalls | Should Be 2 } } It 're-prompts token when invalid authentication has no status code' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:TokenPromptCalls = 0 $script:InvokeRestCalls = 0 function Get-HubspotBearerToken { param ( [Parameter(Mandatory = $false)][string]$AccessToken, [Parameter(Mandatory = $false)][switch]$ForcePrompt ) if ($ForcePrompt.IsPresent) { $script:TokenPromptCalls++ return 'new-token' } return 'old-token' } function Invoke-RestMethod { param ( [Parameter(Mandatory = $false)][uri]$Uri, [Parameter(Mandatory = $false)][string]$Method, [Parameter(Mandatory = $false)][hashtable]$Headers ) $script:InvokeRestCalls++ if ($Headers.Authorization -eq 'Bearer old-token') { $ex = [System.Exception]::new('Authentication credentials not found. This API supports OAuth 2.0 authentication') $errorRecord = [System.Management.Automation.ErrorRecord]::new($ex, 'INVALID_AUTHENTICATION', [System.Management.Automation.ErrorCategory]::AuthenticationError, $null) $errorRecord.ErrorDetails = [System.Management.Automation.ErrorDetails]::new('{"status":"error","category":"INVALID_AUTHENTICATION"}') throw $errorRecord } return [pscustomobject]@{ results = @() } } $null = Invoke-HubspotRequest -ResourcePath 'crm/v3/objects/deals' $script:TokenPromptCalls | Should Be 1 $script:InvokeRestCalls | Should Be 2 } } } |