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.4.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 ($commandNames -contains 'Get-HubspotOwner') | Should Be $true ($commandNames -contains 'Get-HubspotDealCompanies') | Should Be $true ($commandNames -contains 'Set-HubspotDealPrimaryCompany') | Should Be $true ($commandNames -contains 'Get-HubspotCompany') | Should Be $true ($commandNames -contains 'Get-HubspotProperty') | Should Be $true ($commandNames -contains 'New-HubspotProperty') | Should Be $true ($commandNames -contains 'Add-HubspotPropertyOption') | Should Be $true ($commandNames -contains 'Remove-HubspotDealCompany') | Should Be $true } It 'updates configuration in-memory' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop (Get-HubspotConfiguration).ApiVersion | Should Be '2026-09' (Get-HubspotConfiguration).DealsPath | Should Be 'crm/objects/2026-09/deals' $updatedConfiguration = Set-HubspotConfiguration -ServiceUserName 'TEST_HUBSPOT' -BaseUri 'https://api.hubapi.com/' -DealsPath '/crm/objects/2026-09/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/objects/2026-09/deals' $updatedConfiguration.DefaultPageSize | Should Be 200 $updatedConfiguration.DefaultRetryCount | Should Be 5 $updatedConfiguration.DefaultThrottleDelaySeconds | Should Be 0.5 } It 'derives all CRM paths from the configured API version' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop $configuration = Set-HubspotConfiguration -ApiVersion '2027-03' $configuration.ApiVersion | Should Be '2027-03' $configuration.DealsPath | Should Be 'crm/objects/2027-03/deals' InModuleScope InvokeHubspot { Get-HubspotCrmPath -Api 'objects' -Resource 'deals/123' | Should Be 'crm/objects/2027-03/deals/123' Get-HubspotCrmPath -Api 'properties' -Resource 'deals' | Should Be 'crm/properties/2027-03/deals' Get-HubspotCrmPath -Api 'pipelines' -Resource 'deals' | Should Be 'crm/pipelines/2027-03/deals' Get-HubspotCrmPath -Api 'owners' -Resource '87654321' | Should Be 'crm/owners/2027-03/87654321' } (Set-HubspotConfiguration -DealsPath 'custom/deals').DealsPath | Should Be 'custom/deals' (Set-HubspotConfiguration -DealsPath '').DealsPath | Should Be 'crm/objects/2027-03/deals' # Pester 3.4 'Should Throw' does not work under PowerShell 7 - check via try/catch instead. $threw = $false try { $null = Set-HubspotConfiguration -ApiVersion 'v3' -ErrorAction Stop } catch { $threw = $true } $threw | Should Be $true } 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 = '12345678902' 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 = '87654321' } } } function Invoke-HubspotRequest { param ( [Parameter(Mandatory = $false)][string]$ResourcePath, [Parameter(Mandatory = $false)][string]$AccessToken ) if ($ResourcePath -eq 'crm/owners/2026-09/87654321') { return [pscustomobject]@{ id = '87654321' firstName = 'Erika' lastName = 'Musterfrau' email = 'erika.musterfrau@example.org' } } throw "Unexpected resource path: $ResourcePath" } $details = Get-HubspotDealDetails -Id '12345678902' -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 '87654321' $ownerDetail.DisplayValue | Should Be 'Erika Musterfrau' } } 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 = '12345678902' archived = $true properties = [pscustomobject]@{ pipeline = 'default' } } } $details = Get-HubspotDealDetails -Id '12345678902' -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/objects/2026-09/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/objects/2026-09/deals' $script:TokenPromptCalls | Should Be 1 $script:InvokeRestCalls | Should Be 2 } } It 'formats typed values according to the HubSpot property type when writing' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:HubspotPropertyDefinitionsByObjectType = @{ deals = @{ abgabe = [pscustomobject]@{ name = 'abgabe'; type = 'date' } seit = [pscustomobject]@{ name = 'seit'; type = 'datetime' } } } $converted = Convert-HubspotDealPropertiesForWrite -Properties @{ abgabe = [datetime]::new(2026, 10, 14, 0, 0, 0, [DateTimeKind]::Utc) seit = [datetime]::new(2026, 10, 6, 23, 24, 35, [DateTimeKind]::Utc) nda = $false betrag = 1234.5 id = 18380 form = @('E-Mail', 'Portal') text = 'unveraendert' } $converted.abgabe | Should Be '2026-10-14' $converted.seit | Should Be '2026-10-06T23:24:35.000Z' $converted.nda | Should Be 'false' $converted.betrag | Should Be '1234.5' $converted.id | Should Be '18380' $converted.form | Should Be 'E-Mail;Portal' $converted.text | Should Be 'unveraendert' } } It 'searches with exact operators and passes idProperty for unique lookups' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:Calls = @() function Invoke-HubspotRequest { param ([string]$ResourcePath, [string]$Method = 'GET', [hashtable]$Query, $Body, [string]$AccessToken, [int]$RetryCount, [double]$ThrottleDelaySeconds) $script:Calls += [pscustomobject]@{ ResourcePath = $ResourcePath; Method = $Method; Query = $Query; Body = $Body } return [pscustomobject]@{ id = '1'; properties = [pscustomobject]@{}; results = @() } } function Convert-HubspotRecordToObject { param ($Record, [string]$AccessToken) return $Record } $null = Search-HubspotDeals -PropertyName 'zep_angebots_id' -Operator 'EQ' -SearchTerm '18380' $filter = $script:Calls[0].Body.filterGroups[0].filters[0] $script:Calls[0].ResourcePath | Should Be 'crm/objects/2026-09/deals/search' $filter.operator | Should Be 'EQ' $filter.value | Should Be '18380' $null = Search-HubspotDeals -PropertyName 'zep_angebots_id' -Operator 'HAS_PROPERTY' $script:Calls[1].Body.filterGroups[0].filters[0].ContainsKey('value') | Should Be $false $threw = $false try { $null = Search-HubspotDeals -PropertyName 'dealname' -Operator 'EQ' -ErrorAction Stop } catch { $threw = $true } $threw | Should Be $true $null = Get-HubspotDeal -Id '18380' -IdProperty 'zep_angebots_id' $script:Calls[2].ResourcePath | Should Be 'crm/objects/2026-09/deals/18380' $script:Calls[2].Query.idProperty | Should Be 'zep_angebots_id' } } It 'resolves owners by email with session cache' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:OwnerCalls = 0 function Invoke-HubspotRequest { param ([string]$ResourcePath, [string]$Method = 'GET', [hashtable]$Query, $Body, [string]$AccessToken) $script:OwnerCalls++ $ResourcePath | Should Be 'crm/owners/2026-09' $Query.email | Should Be 'max.mustermann@contoso.com' return [pscustomobject]@{ results = @([pscustomobject]@{ id = '12345678'; email = 'max.mustermann@contoso.com'; firstName = 'Max'; lastName = 'Mustermann'; userId = 12345678; archived = $false }) } } (Get-HubspotOwner -Email 'max.mustermann@contoso.com').Id | Should Be '12345678' (Get-HubspotOwner -Email ' Max.Mustermann@contoso.com ').Id | Should Be '12345678' $script:OwnerCalls | Should Be 1 } } It 'sets the primary company and removes the previous primary company' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:Calls = @() function Invoke-HubspotRequest { param ([string]$ResourcePath, [string]$Method = 'GET', [hashtable]$Query, $Body, [string]$AccessToken, [int]$RetryCount, [double]$ThrottleDelaySeconds) $script:Calls += [pscustomobject]@{ ResourcePath = $ResourcePath; Method = $Method; Body = $Body } if ($Method -eq 'GET') { return [pscustomobject]@{ results = @( [pscustomobject]@{ toObjectId = 111; associationTypes = @([pscustomobject]@{ typeId = 5; label = 'Primary' }, [pscustomobject]@{ typeId = 341; label = $null }) }, [pscustomobject]@{ toObjectId = 333; associationTypes = @([pscustomobject]@{ typeId = 341; label = $null }) } ) } } } $companies = @(Get-HubspotDealCompanies -DealId '42') $companies.Count | Should Be 2 ($companies | Where-Object IsPrimary).CompanyId | Should Be '111' $script:Calls = @() Set-HubspotDealPrimaryCompany -DealId '42' -CompanyId '222' -RemovePreviousPrimary $put = $script:Calls | Where-Object Method -eq 'PUT' $put.ResourcePath | Should Be 'crm/objects/2026-09/deals/42/associations/companies/222' (@($put.Body) | ForEach-Object { $_.associationTypeId }) -join ',' | Should Be '5,341' ($script:Calls | Where-Object Method -eq 'DELETE').ResourcePath | Should Be 'crm/objects/2026-09/deals/42/associations/companies/111' } } It 'searches with IN values and sorting' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:Calls = @() function Invoke-HubspotRequest { param ([string]$ResourcePath, [string]$Method = 'GET', [hashtable]$Query, $Body, [string]$AccessToken, [int]$RetryCount, [double]$ThrottleDelaySeconds) $script:Calls += [pscustomobject]@{ Body = $Body } return [pscustomobject]@{ results = @() } } $null = Search-HubspotDeals -PropertyName 'zep_status' -Operator 'IN' -Values '10', '20' -SortProperty 'zep_angebots_id' -SortDescending $filter = $script:Calls[0].Body.filterGroups[0].filters[0] $filter.operator | Should Be 'IN' (@($filter.values) -join ',') | Should Be '10,20' $filter.ContainsKey('value') | Should Be $false $script:Calls[0].Body.sorts[0].propertyName | Should Be 'zep_angebots_id' $script:Calls[0].Body.sorts[0].direction | Should Be 'DESCENDING' $threw = $false try { $null = Search-HubspotDeals -PropertyName 'zep_status' -Operator 'IN' -ErrorAction Stop } catch { $threw = $true } $threw | Should Be $true } } It 'adds a property option while keeping existing options' { Remove-Module InvokeHubspot -ErrorAction SilentlyContinue Import-Module $manifestPath -Force -ErrorAction Stop InModuleScope InvokeHubspot { $script:Patch = $null function Invoke-HubspotRequest { param ([string]$ResourcePath, [string]$Method = 'GET', [hashtable]$Query, $Body, [string]$AccessToken, [int]$RetryCount, [double]$ThrottleDelaySeconds) if ($Method -eq 'PATCH') { $script:Patch = [pscustomobject]@{ ResourcePath = $ResourcePath; Body = $Body }; return $null } return [pscustomobject]@{ name = 'zep_status'; options = @([pscustomobject]@{ value = '10'; label = 'neu'; displayOrder = 0; hidden = $false }) } } $null = Add-HubspotPropertyOption -Name 'zep_status' -Value '10' -Label 'neu' $script:Patch | Should Be $null $null = Add-HubspotPropertyOption -Name 'zep_status' -Value '300' -Label 'Neu in ZEP' $script:Patch.ResourcePath | Should Be 'crm/properties/2026-09/deals/zep_status' (@($script:Patch.Body.options | ForEach-Object { $_.value }) -join ',') | Should Be '10,300' $script:Patch.Body.options[1].displayOrder | Should Be 1 $script:Patch = $null $null = Add-HubspotPropertyOption -Name 'zep_status' -Value '10' -Label 'neu (ZEP)' $script:Patch | Should Be $null $null = Add-HubspotPropertyOption -Name 'zep_status' -Value '10' -Label 'neu (ZEP)' -UpdateLabel @($script:Patch.Body.options).Count | Should Be 1 $script:Patch.Body.options[0].value | Should Be '10' $script:Patch.Body.options[0].label | Should Be 'neu (ZEP)' } } } |