Tests/ITSM-Insights.Tests.ps1
|
BeforeAll { $ModuleRoot = Split-Path -Parent $PSScriptRoot $ModuleName = 'ITSM-Insights' $ManifestPath = Join-Path $ModuleRoot "$ModuleName.psd1" # Remove module if already loaded Get-Module $ModuleName -ErrorAction SilentlyContinue | Remove-Module -Force # Import module Import-Module $ManifestPath -Force -ErrorAction Stop } AfterAll { Get-Module 'ITSM-Insights' -ErrorAction SilentlyContinue | Remove-Module -Force } Describe 'Module Loading' { It 'Should import the module without errors' { $module = Get-Module 'ITSM-Insights' $module | Should -Not -BeNullOrEmpty $module.Name | Should -Be 'ITSM-Insights' } It 'Should export exactly 5 public functions' { $exported = (Get-Module 'ITSM-Insights').ExportedFunctions.Keys $exported.Count | Should -Be 5 } It 'Should export Get-CIHistory' { Get-Command -Module 'ITSM-Insights' -Name 'Get-CIHistory' | Should -Not -BeNullOrEmpty } It 'Should export Get-UserTicketHistory' { Get-Command -Module 'ITSM-Insights' -Name 'Get-UserTicketHistory' | Should -Not -BeNullOrEmpty } It 'Should export Get-RecurringIssues' { Get-Command -Module 'ITSM-Insights' -Name 'Get-RecurringIssues' | Should -Not -BeNullOrEmpty } It 'Should export Get-KnowledgeGaps' { Get-Command -Module 'ITSM-Insights' -Name 'Get-KnowledgeGaps' | Should -Not -BeNullOrEmpty } It 'Should export Sync-KnowledgeArticles' { Get-Command -Module 'ITSM-Insights' -Name 'Sync-KnowledgeArticles' | Should -Not -BeNullOrEmpty } It 'Should NOT export private functions' { $exported = (Get-Module 'ITSM-Insights').ExportedFunctions.Keys $exported | Should -Not -Contain 'Invoke-AICompletion' $exported | Should -Not -Contain 'Connect-ServiceNow' $exported | Should -Not -Contain 'Connect-JiraSM' $exported | Should -Not -Contain 'Import-TicketExport' $exported | Should -Not -Contain 'New-HtmlDashboard' } } Describe 'Module Manifest' { BeforeAll { $manifest = Test-ModuleManifest -Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'ITSM-Insights.psd1') } It 'Should have the correct GUID' { $manifest.GUID | Should -Be 'c9d0e1f2-6a57-4cd3-b4e5-0a1b2c3d4e56' } It 'Should require PowerShell 5.1 or higher' { $manifest.PowerShellVersion | Should -Be '5.1' } It 'Should have the correct author' { $manifest.Author | Should -BeLike '*Larry Roberts*' } It 'Should have a description' { $manifest.Description | Should -Not -BeNullOrEmpty $manifest.Description | Should -BeLike '*ITSM*' } It 'Should have required tags' { $tags = $manifest.PrivateData.PSData.Tags $tags | Should -Contain 'ServiceNow' $tags | Should -Contain 'ITSM' $tags | Should -Contain 'ITIL' $tags | Should -Contain 'AI' $tags | Should -Contain 'KnowledgeBase' $tags | Should -Contain 'Incidents' $tags | Should -Contain 'ChangeManagement' } It 'Should have ProjectUri set' { $manifest.PrivateData.PSData.ProjectUri | Should -Not -BeNullOrEmpty } It 'Should have LicenseUri set' { $manifest.PrivateData.PSData.LicenseUri | Should -Not -BeNullOrEmpty } It 'Should export the correct functions' { $manifest.ExportedFunctions.Keys | Should -Contain 'Get-CIHistory' $manifest.ExportedFunctions.Keys | Should -Contain 'Get-UserTicketHistory' $manifest.ExportedFunctions.Keys | Should -Contain 'Get-RecurringIssues' $manifest.ExportedFunctions.Keys | Should -Contain 'Get-KnowledgeGaps' $manifest.ExportedFunctions.Keys | Should -Contain 'Sync-KnowledgeArticles' } } Describe 'Get-CIHistory Parameter Validation' { It 'Should require CIName parameter' { $cmd = Get-Command -Module 'ITSM-Insights' -Name 'Get-CIHistory' $param = $cmd.Parameters['CIName'] $param | Should -Not -BeNullOrEmpty $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.Mandatory } | Should -Not -BeNullOrEmpty } It 'Should validate Provider parameter with ValidateSet' { $cmd = Get-Command -Module 'ITSM-Insights' -Name 'Get-CIHistory' $param = $cmd.Parameters['Provider'] $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } $validateSet | Should -Not -BeNullOrEmpty $validateSet.ValidValues | Should -Contain 'ServiceNow' $validateSet.ValidValues | Should -Contain 'Jira' $validateSet.ValidValues | Should -Contain 'File' } It 'Should validate AIProvider parameter with ValidateSet' { $cmd = Get-Command -Module 'ITSM-Insights' -Name 'Get-CIHistory' $param = $cmd.Parameters['AIProvider'] $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } $validateSet | Should -Not -BeNullOrEmpty $validateSet.ValidValues | Should -Contain 'Anthropic' $validateSet.ValidValues | Should -Contain 'OpenAI' $validateSet.ValidValues | Should -Contain 'Ollama' $validateSet.ValidValues | Should -Contain 'Custom' } It 'Should validate MonthsBack range (1-60)' { $cmd = Get-Command -Module 'ITSM-Insights' -Name 'Get-CIHistory' $param = $cmd.Parameters['MonthsBack'] $validateRange = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateRangeAttribute] } $validateRange | Should -Not -BeNullOrEmpty $validateRange.MinRange | Should -Be 1 $validateRange.MaxRange | Should -Be 60 } It 'Should have SkipAI switch parameter' { $cmd = Get-Command -Module 'ITSM-Insights' -Name 'Get-CIHistory' $param = $cmd.Parameters['SkipAI'] $param | Should -Not -BeNullOrEmpty $param.SwitchParameter | Should -Be $true } } Describe 'Get-UserTicketHistory Parameter Validation' { It 'Should require UserIdentity parameter' { $cmd = Get-Command -Module 'ITSM-Insights' -Name 'Get-UserTicketHistory' $param = $cmd.Parameters['UserIdentity'] $param | Should -Not -BeNullOrEmpty $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.Mandatory } | Should -Not -BeNullOrEmpty } It 'Should validate Role parameter with ValidateSet' { $cmd = Get-Command -Module 'ITSM-Insights' -Name 'Get-UserTicketHistory' $param = $cmd.Parameters['Role'] $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } $validateSet | Should -Not -BeNullOrEmpty $validateSet.ValidValues | Should -Contain 'All' $validateSet.ValidValues | Should -Contain 'Requester' $validateSet.ValidValues | Should -Contain 'Assignee' $validateSet.ValidValues | Should -Contain 'Both' } } Describe 'Get-RecurringIssues Parameter Validation' { It 'Should have MinOccurrences parameter' { $cmd = Get-Command -Module 'ITSM-Insights' -Name 'Get-RecurringIssues' $param = $cmd.Parameters['MinOccurrences'] $param | Should -Not -BeNullOrEmpty } It 'Should have Category parameter' { $cmd = Get-Command -Module 'ITSM-Insights' -Name 'Get-RecurringIssues' $param = $cmd.Parameters['Category'] $param | Should -Not -BeNullOrEmpty } } Describe 'Sync-KnowledgeArticles ShouldProcess' { It 'Should support ShouldProcess (WhatIf)' { $cmd = Get-Command -Module 'ITSM-Insights' -Name 'Sync-KnowledgeArticles' $cmd.Parameters.ContainsKey('WhatIf') | Should -Be $true $cmd.Parameters.ContainsKey('Confirm') | Should -Be $true } It 'Should accept pipeline input for Articles' { $cmd = Get-Command -Module 'ITSM-Insights' -Name 'Sync-KnowledgeArticles' $param = $cmd.Parameters['Articles'] $pipelineAttr = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.ValueFromPipeline } $pipelineAttr | Should -Not -BeNullOrEmpty } } Describe 'Connect-ServiceNow (Mocked)' { BeforeAll { # Access private function via module scope $snowFunc = & (Get-Module 'ITSM-Insights') { Get-Command Connect-ServiceNow } } It 'Should throw when no authentication is provided' { # Clear environment variable temporarily $origKey = $env:SNOW_API_KEY $env:SNOW_API_KEY = $null try { { & (Get-Module 'ITSM-Insights') { Connect-ServiceNow -Instance 'test.service-now.com' -Method GET -Endpoint 'api/now/table/incident' } } | Should -Throw '*authentication*' } finally { $env:SNOW_API_KEY = $origKey } } It 'Should construct correct URL from instance name' { Mock -ModuleName 'ITSM-Insights' Invoke-RestMethod { return @{ result = @() } } & (Get-Module 'ITSM-Insights') { Connect-ServiceNow -Instance 'mycompany.service-now.com' -Method GET ` -Endpoint 'api/now/table/incident' -ApiKey 'test-key' ` -QueryParameters @{ sysparm_query = 'active=true' } } Should -Invoke -ModuleName 'ITSM-Insights' Invoke-RestMethod -Times 1 -ParameterFilter { $Uri -like 'https://mycompany.service-now.com/api/now/table/incident*' } } It 'Should include correct auth header for API key' { Mock -ModuleName 'ITSM-Insights' Invoke-RestMethod { return @{ result = @() } } & (Get-Module 'ITSM-Insights') { Connect-ServiceNow -Instance 'test.service-now.com' -Method GET ` -Endpoint 'api/now/table/incident' -ApiKey 'my-secret-key' } Should -Invoke -ModuleName 'ITSM-Insights' Invoke-RestMethod -Times 1 -ParameterFilter { $Headers['Authorization'] -eq 'Bearer my-secret-key' } } It 'Should handle pagination correctly' { $callCount = 0 Mock -ModuleName 'ITSM-Insights' Invoke-RestMethod { $script:callCount++ if ($Uri -match 'sysparm_offset=0') { return @{ result = @( @{ number = 'INC0000001'; short_description = 'Test 1' } @{ number = 'INC0000002'; short_description = 'Test 2' } ) } } else { return @{ result = @() } } } $results = & (Get-Module 'ITSM-Insights') { Connect-ServiceNow -Instance 'test.service-now.com' -Method GET ` -Endpoint 'api/now/table/incident' -ApiKey 'test-key' -Paginate -PageSize 2 } $results | Should -Not -BeNullOrEmpty } } Describe 'Get-CIHistory with File Provider (Mocked)' { BeforeAll { $samplesDir = Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples' $samplePath = Join-Path $samplesDir 'sample-tickets.json' } It 'Should return correct ticket counts from sample data with SkipAI' { $result = Get-CIHistory -CIName 'SQL-PROD-01' -Provider File -FilePath $samplePath -SkipAI -MonthsBack 60 $result | Should -Not -BeNullOrEmpty $result.CIName | Should -Be 'SQL-PROD-01' $result.TicketCount | Should -BeGreaterThan 0 $result.IncidentCount | Should -BeGreaterThan 0 $result.ChangeCount | Should -BeGreaterThan 0 $result.ProblemCount | Should -BeGreaterThan 0 } It 'Should identify open items' { $result = Get-CIHistory -CIName 'SQL-PROD-01' -Provider File -FilePath $samplePath -SkipAI -MonthsBack 60 $result.OpenItems | Should -Not -BeNullOrEmpty $openNumbers = $result.OpenItems | ForEach-Object { $_.Number } $openNumbers | Should -Contain 'PRB0001150' } It 'Should return a timeline' { $result = Get-CIHistory -CIName 'SQL-PROD-01' -Provider File -FilePath $samplePath -SkipAI -MonthsBack 60 $result.Timeline | Should -Not -BeNullOrEmpty $result.Timeline.Count | Should -Be $result.TicketCount } It 'Should throw when FilePath is missing for File provider' { { Get-CIHistory -CIName 'SQL-PROD-01' -Provider File -SkipAI } | Should -Throw '*FilePath*' } It 'Should have empty summary when SkipAI is used' { $result = Get-CIHistory -CIName 'SQL-PROD-01' -Provider File -FilePath $samplePath -SkipAI -MonthsBack 60 $result.Summary | Should -BeNullOrEmpty } It 'Should generate HTML report when OutputPath is specified' { $tempHtml = Join-Path $env:TEMP 'itsm-test-ci-report.html' try { $result = Get-CIHistory -CIName 'SQL-PROD-01' -Provider File -FilePath $samplePath -SkipAI -MonthsBack 60 -OutputPath $tempHtml Test-Path $tempHtml | Should -Be $true $htmlContent = Get-Content -Path $tempHtml -Raw $htmlContent | Should -BeLike '*SQL-PROD-01*' } finally { if (Test-Path $tempHtml) { Remove-Item $tempHtml -Force } } } } Describe 'Get-RecurringIssues with File Provider' { BeforeAll { $samplesDir = Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples' $samplePath = Join-Path $samplesDir 'sample-tickets.json' } It 'Should detect disk space as a recurring pattern' { $result = Get-RecurringIssues -Provider File -FilePath $samplePath -MinOccurrences 2 -SkipAI -MonthsBack 60 $result | Should -Not -BeNullOrEmpty $diskPattern = $result | Where-Object { $_.Pattern -like '*disk*' -or $_.Pattern -like '*Hardware*' } $diskPattern | Should -Not -BeNullOrEmpty } It 'Should return structured pattern objects' { $result = Get-RecurringIssues -Provider File -FilePath $samplePath -MinOccurrences 2 -SkipAI -MonthsBack 60 foreach ($pattern in $result) { $pattern.Pattern | Should -Not -BeNullOrEmpty $pattern.Occurrences | Should -BeGreaterOrEqual 2 $pattern.TicketNumbers | Should -Not -BeNullOrEmpty $pattern.SuggestedResolution | Should -Not -BeNullOrEmpty } } } Describe 'Import-TicketExport' { It 'Should import JSON files correctly' { $samplesDir = Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples' $sampleJsonPath = Join-Path $samplesDir 'sample-tickets.json' InModuleScope 'ITSM-Insights' -Parameters @{ SamplePath = $sampleJsonPath } { param($SamplePath) $results = Import-TicketExport -Path $SamplePath -MonthsBack 60 $results | Should -Not -BeNullOrEmpty $results.Count | Should -BeGreaterThan 0 } } It 'Should normalize column names from JSON' { $samplesDir = Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples' $sampleJsonPath = Join-Path $samplesDir 'sample-tickets.json' InModuleScope 'ITSM-Insights' -Parameters @{ SamplePath = $sampleJsonPath } { param($SamplePath) $results = Import-TicketExport -Path $SamplePath -MonthsBack 60 $firstTicket = $results | Select-Object -First 1 $firstTicket.PSObject.Properties.Name | Should -Contain 'Number' $firstTicket.PSObject.Properties.Name | Should -Contain 'ShortDescription' $firstTicket.PSObject.Properties.Name | Should -Contain 'State' $firstTicket.PSObject.Properties.Name | Should -Contain 'OpenedAt' } } It 'Should filter by CI name' { $samplesDir = Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples' $sampleJsonPath = Join-Path $samplesDir 'sample-tickets.json' InModuleScope 'ITSM-Insights' -Parameters @{ SamplePath = $sampleJsonPath } { param($SamplePath) $results = Import-TicketExport -Path $SamplePath -CIFilter 'SQL-PROD-01' -MonthsBack 60 $results | Should -Not -BeNullOrEmpty foreach ($ticket in $results) { ($ticket.CIName -like '*SQL-PROD-01*' -or $ticket.ShortDescription -like '*SQL-PROD-01*' -or $ticket.Description -like '*SQL-PROD-01*') | Should -Be $true } } } It 'Should handle CSV files' { $csvPath = Join-Path $env:TEMP 'test-tickets.csv' try { @" number,short_description,state,priority,opened_at,category,assigned_to,cmdb_ci,type INC0099001,Test incident one,Closed,2 - High,2025-01-15 10:00:00,Hardware,John Doe,TEST-SRV-01,Incident INC0099002,Test incident two,Open,3 - Moderate,2025-06-01 08:00:00,Software,Jane Smith,TEST-SRV-01,Incident "@ | Out-File -FilePath $csvPath -Encoding UTF8 InModuleScope 'ITSM-Insights' -Parameters @{ CsvPath = $csvPath } { param($CsvPath) $results = Import-TicketExport -Path $CsvPath -MonthsBack 60 $results | Should -Not -BeNullOrEmpty $results.Count | Should -Be 2 $results[0].Number | Should -Be 'INC0099001' } } finally { if (Test-Path $csvPath) { Remove-Item $csvPath -Force } } } It 'Should throw for unsupported file formats' { { InModuleScope 'ITSM-Insights' { Import-TicketExport -Path 'C:\fake\file.xml' -MonthsBack 12 } } | Should -Throw } } Describe 'Sync-KnowledgeArticles WhatIf and ReviewOnly' { It 'Should generate local files in ReviewOnly mode' { $outputDir = Join-Path $env:TEMP 'itsm-test-kb-review' try { $articles = @( [PSCustomObject]@{ GapType = 'Missing' Topic = 'Test Topic' RelatedTickets = @('INC0000001', 'INC0000002') SuggestedTitle = 'How to Handle Test Issues' SuggestedContent = 'Step 1: Do this. Step 2: Do that.' } ) $result = $articles | Sync-KnowledgeArticles -ReviewOnly -OutputDirectory $outputDir $result | Should -Not -BeNullOrEmpty $result[0].Status | Should -Be 'ReviewPending' Test-Path $outputDir | Should -Be $true (Get-ChildItem $outputDir -Filter '*.md').Count | Should -Be 1 } finally { if (Test-Path $outputDir) { Remove-Item $outputDir -Recurse -Force } } } It 'Should not push to ServiceNow when WhatIf is used' { Mock -ModuleName 'ITSM-Insights' Connect-ServiceNow { throw 'Should not be called with WhatIf' } $articles = @( [PSCustomObject]@{ SuggestedTitle = 'WhatIf Test Article' SuggestedContent = 'Test content' GapType = 'Missing' } ) $result = $articles | Sync-KnowledgeArticles -Instance 'test.service-now.com' ` -ApiKey 'test-key' -KnowledgeBase 'kb-sys-id-123' -WhatIf $result | Should -Not -BeNullOrEmpty $result[0].Status | Should -Be 'WhatIf' # Connect-ServiceNow should NOT have been called Should -Not -Invoke -ModuleName 'ITSM-Insights' Connect-ServiceNow } It 'Should always create articles as DRAFT when pushing' { Mock -ModuleName 'ITSM-Insights' Connect-ServiceNow { # Verify the body contains workflow_state = draft $Body['workflow_state'] | Should -Be 'draft' return @{ result = @{ sys_id = 'abc123' number = 'KB0001001' } } } $articles = @( [PSCustomObject]@{ SuggestedTitle = 'Draft Test Article' SuggestedContent = 'Content here' GapType = 'Missing' } ) $result = $articles | Sync-KnowledgeArticles -Instance 'test.service-now.com' ` -ApiKey 'test-key' -KnowledgeBase 'kb-sys-id-123' -Confirm:$false Should -Invoke -ModuleName 'ITSM-Insights' Connect-ServiceNow -Times 1 $result[0].Status | Should -Be 'Created' } } Describe 'Template Files' { It 'Should have ci-history-prompt.txt template' { $templatesDir = Join-Path (Split-Path -Parent $PSScriptRoot) 'Templates' $templatePath = Join-Path $templatesDir 'ci-history-prompt.txt' Test-Path $templatePath | Should -Be $true $content = Get-Content $templatePath -Raw $content | Should -BeLike '*{ci_name}*' $content | Should -BeLike '*{ticket_data}*' } It 'Should have user-history-prompt.txt template' { $templatesDir = Join-Path (Split-Path -Parent $PSScriptRoot) 'Templates' $templatePath = Join-Path $templatesDir 'user-history-prompt.txt' Test-Path $templatePath | Should -Be $true $content = Get-Content $templatePath -Raw $content | Should -BeLike '*{user_name}*' $content | Should -BeLike '*{ticket_data}*' } It 'Should have recurring-issues-prompt.txt template' { $templatesDir = Join-Path (Split-Path -Parent $PSScriptRoot) 'Templates' $templatePath = Join-Path $templatesDir 'recurring-issues-prompt.txt' Test-Path $templatePath | Should -Be $true $content = Get-Content $templatePath -Raw $content | Should -BeLike '*{min_occurrences}*' $content | Should -BeLike '*{ticket_data}*' } It 'Should have knowledge-gap-prompt.txt template' { $templatesDir = Join-Path (Split-Path -Parent $PSScriptRoot) 'Templates' $templatePath = Join-Path $templatesDir 'knowledge-gap-prompt.txt' Test-Path $templatePath | Should -Be $true $content = Get-Content $templatePath -Raw $content | Should -BeLike '*{ticket_data}*' $content | Should -BeLike '*{kb_articles}*' } } Describe 'Sample Data' { It 'Should have valid sample-tickets.json' { $samplesDir = Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples' $samplePath = Join-Path $samplesDir 'sample-tickets.json' Test-Path $samplePath | Should -Be $true $content = Get-Content $samplePath -Raw $tickets = $content | ConvertFrom-Json $tickets | Should -Not -BeNullOrEmpty $tickets.Count | Should -BeGreaterOrEqual 15 # Verify expected ticket types $types = $tickets | ForEach-Object { $_.type } | Sort-Object -Unique $types | Should -Contain 'Incident' $types | Should -Contain 'Change Request' $types | Should -Contain 'Problem' } It 'Should have sample CI report HTML' { $samplesDir = Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples' $samplePath = Join-Path $samplesDir 'sample-ci-report.html' Test-Path $samplePath | Should -Be $true $content = Get-Content $samplePath -Raw $content | Should -BeLike '*SQL-PROD-01*' $content | Should -BeLike '*#2dd4bf*' } It 'Should have sample recurring report HTML' { $samplesDir = Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples' $samplePath = Join-Path $samplesDir 'sample-recurring-report.html' Test-Path $samplePath | Should -Be $true $content = Get-Content $samplePath -Raw $content | Should -BeLike '*Recurring*' $content | Should -BeLike '*Disk*' } } |