public/Invoke-MyCorp.ps1
|
<#
.SYNOPSIS Main MyCorp command that runs tests and generates reports. .DESCRIPTION Invoke-MyCorp runs Pester tests and generates reports in HTML, Markdown, JSON, CSV and Excel formats. #> function Invoke-MyCorp { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Colors are helpful for UX')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'False positives for ExportCsv/ExportExcel')] [CmdletBinding()] param ( [Parameter(Position = 0)] [string] $Path, [string[]] $Tag, [string[]] $ExcludeTag, [string] $OutputHtmlFile, [string] $OutputMarkdownFile, [string] $OutputJsonFile, [string] $OutputFolder, [string] $OutputFolderFileName, [string] $SubscriptionId, [PesterConfiguration] $PesterConfiguration, [ValidateSet('None','Normal','Detailed','Diagnostic')] [string] $Verbosity = 'None', [switch] $NonInteractive, [switch] $PassThru, [string[]] $MailRecipient, [string] $MailTestResultsUri, [string] $MailUserId, [string] $TeamId, [string] $TeamChannelId, [string] $TeamChannelWebhookUri, [switch] $SkipGraphConnect, [switch] $DisableTelemetry, [switch] $SkipVersionCheck, [switch] $ExportCsv, [switch] $ExportExcel, [switch] $NoLogo, [string] $DriftRoot ) # ----------------------- # Session and helper init # ----------------------- if (-not (Test-Path variable:\__MyCorpSession)) { New-Variable -Name __MyCorpSession -Value ([PSCustomObject]@{ Connections = @() MyCorpConfig = $null }) -Scope Script -Force | Out-Null } function GetDefaultFileName { $timestamp = Get-Date -Format "yyyy-MM-dd-HHmmss" return "TestResults-$timestamp.html" } function ValidateAndSetOutputFiles { param($out) $result = $null if (-not [string]::IsNullOrEmpty($out.OutputHtmlFile) -and ($out.OutputHtmlFile -notlike '*.html')) { $result = 'The OutputHtmlFile parameter must have an .html extension.' return $result } if (-not [string]::IsNullOrEmpty($out.OutputMarkdownFile) -and ($out.OutputMarkdownFile -notlike '*.md')) { $result = 'The OutputMarkdownFile parameter must have an .md extension.' return $result } if (-not [string]::IsNullOrEmpty($out.OutputJsonFile) -and ($out.OutputJsonFile -notlike '*.json')) { $result = 'The OutputJsonFile parameter must have a .json extension.' return $result } $someOutputFileHasValue = -not [string]::IsNullOrEmpty($out.OutputHtmlFile) -or ` -not [string]::IsNullOrEmpty($out.OutputMarkdownFile) -or -not [string]::IsNullOrEmpty($out.OutputJsonFile) if ([string]::IsNullOrEmpty($out.OutputFolder) -and -not $someOutputFileHasValue) { $out.OutputFolder = './test-results' } if (-not [string]::IsNullOrEmpty($out.OutputFolder)) { try { New-Item -Path $out.OutputFolder -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null } catch {} if ([string]::IsNullOrEmpty($out.OutputFolderFileName)) { $out.OutputFolderFileName = "TestResults-$(Get-Date -Format 'yyyy-MM-dd-HHmmss')" } $out.OutputHtmlFile = Join-Path $out.OutputFolder "$($out.OutputFolderFileName).html" $out.OutputMarkdownFile = Join-Path $out.OutputFolder "$($out.OutputFolderFileName).md" $out.OutputJsonFile = Join-Path $out.OutputFolder "$($out.OutputFolderFileName).json" if ($ExportCsv.IsPresent) { $out.OutputCsvFile = Join-Path $out.OutputFolder "$($out.OutputFolderFileName).csv" } if ($ExportExcel.IsPresent) { $out.OutputExcelFile = Join-Path $out.OutputFolder "$($out.OutputFolderFileName).xlsx" } } return $result } function GetPesterConfiguration { param($Path, $Tag, $ExcludeTag, $PesterConfiguration) if (-not $PesterConfiguration) { $PesterConfiguration = New-PesterConfiguration } $PesterConfiguration.Run.PassThru = $true $PesterConfiguration.Output.Verbosity = $Verbosity if ($Path) { $PesterConfiguration.Run.Path = $Path } else { if (Test-Path -Path "./powershell/tests/pester.ps1") { $PesterConfiguration.Run.Path = './tests' } else { $PesterConfiguration.Run.Path = './' } } if ($Tag) { $PesterConfiguration.Filter.Tag = $Tag } if ($ExcludeTag) { $PesterConfiguration.Filter.ExcludeTag = $ExcludeTag } return $PesterConfiguration } # ----------------------- # Version, banner and telemetry # ----------------------- $version = $null try { $version = (Get-MtModuleVersion) } catch { $version = 'Unknown' } if ($NonInteractive.IsPresent -or $NoLogo.IsPresent) { Write-Verbose "Running MyCorp v$version" } else { $banner = @' $$\ $$\ $$$$$$\ $$$\ $$$ | $$ __$$\ $$$$\ $$$$ |$$\ $$\ $$ / \__| $$$$$$\ $$$$$$\ $$$$$$\ $$\$$\$$ $$ |$$ | $$ |$$ | $$ __$$\ $$ __$$\ $$ __$$\ $$ \$$$ $$ |$$ | $$ |$$ | $$ / $$ |$$ | \__|$$ / $$ | $$ |\$ /$$ |$$ | $$ |$$ | $$\ $$ | $$ |$$ | $$ | $$ | $$ | \_/ $$ |\$$$$$$$ |\$$$$$$ |\$$$$$$ |$$ | $$$$$$$ | \__| \__| \____$$ | \______/ \______/ \__| $$ ____/ $$\ $$ | $$ | \$$$$$$ | $$ | \______/ \__| '@ Write-Host $banner -ForegroundColor Red Write-Host "MyCorp Test Runner v$version" -ForegroundColor Red } # Telemetry: try safe event names and swallow failures (validation may reject unknown names) if (-not $DisableTelemetry) { try { Write-Telemetry -EventName 'InvokeMyCorp' } catch { try { Write-Telemetry -EventName 'InvokeMaester' } catch { } } } # ----------------------- # Graph connection check # ----------------------- $isMail = $null -ne $MailRecipient $isTeamsChannelMessage = -not ([String]::IsNullOrEmpty($TeamId) -or [String]::IsNullOrEmpty($TeamChannelId)) if ($SkipGraphConnect) { if (-not $NonInteractive.IsPresent) { Write-Host "🔥 Skipping graph connection check" -ForegroundColor Yellow } } else { try { $ok = Test-MtContext -SendMail:$isMail -SendTeamsMessage:$isTeamsChannelMessage if (-not $ok) { return } } catch { Write-Error $_ return } } # Initialize session - prefer new name but fall back to Mt* try { if (Get-Command -Name Initialize-MyCorpSession -ErrorAction SilentlyContinue) { Initialize-MyCorpSession } elseif (Get-Command -Name Initialize-MtSession -ErrorAction SilentlyContinue) { Initialize-MtSession } } catch { Write-Verbose "Warning: session initialization failed or not found: $_" } # --------------------------------------------------------- # Azure Subscription Discovery for Subscription-Wise Testing # --------------------------------------------------------- try { $subs = Get-AzSubscription -ErrorAction Stop | Select-Object Id, Name, TenantId, State $__MyCorpSession.Subscriptions = $subs if ($subs.Count -eq 0) { Write-Verbose 'No Azure subscriptions found.' } elseif ($subs.Count -eq 1) { $__MyCorpSession.DefaultSubscription = $subs[0] $__MyCorpSession.SelectedSubscription = $subs[0] Set-AzContext -SubscriptionId $subs[0].Id -ErrorAction SilentlyContinue Write-Verbose "Using single subscription: $($subs[0].Name) ($($subs[0].Id))" } else { Write-Verbose "Discovered $($subs.Count) Azure subscriptions." Write-Host "Available Azure Subscriptions:" -ForegroundColor Cyan $i = 0 foreach ($s in $subs) { $i++ Write-Host ("[{0}] {1} ({2})" -f $i, $s.Name, $s.Id) } Write-Host "`nCall Select-MyCorpSubscription -Index <number> to choose a subscription." -ForegroundColor Yellow } } catch { Write-Warning "Failed to enumerate Azure subscriptions: $($_.Exception.Message)" $__MyCorpSession.Subscriptions = @() } # Validate webhook url if provided if (-not [string]::IsNullOrEmpty($TeamChannelWebhookUri)) { $urlPattern = '^(https)://[^\s/$.?#].[^\s]*$' if (-not ($TeamChannelWebhookUri -match $urlPattern)) { Write-Error -Message "Invalid Webhook URL: $TeamChannelWebhookUri" return } } # ----------------------- # Output file handling # ----------------------- $out = [PSCustomObject]@{ OutputFolder = $OutputFolder OutputFolderFileName = $OutputFolderFileName OutputHtmlFile = $OutputHtmlFile OutputMarkdownFile = $OutputMarkdownFile OutputJsonFile = $OutputJsonFile OutputCsvFile = $null OutputExcelFile = $null } $result = ValidateAndSetOutputFiles $out if ($result) { Write-Error -Message $result; return } # ----------------------- # Tags defaults and compatibility # ----------------------- if (-not $Tag) { $Tag = @() } if (-not $ExcludeTag) { $ExcludeTag = @() } # Backwards-compat defaults if ("CAWhatIf" -notin $Tag) { $ExcludeTag += 'CAWhatIf' } if (-not $Tag -or $Tag.Count -eq 0) { $ExcludeTag += 'Full' } if ('Full' -in $Tag) { $Tag += 'All' } # ----------------------- # Path validation # ----------------------- $pesterConfig = GetPesterConfiguration -Path $Path -Tag $Tag -ExcludeTag $ExcludeTag -PesterConfiguration $PesterConfiguration $Path = $pesterConfig.Run.Path.value Write-Verbose "Merged configuration: $($pesterConfig | ConvertTo-Json -Depth 5 -Compress)" if (Test-Path -Path $Path -PathType Leaf) { if ($NonInteractive.IsPresent) { Write-Error -Message "The path '$Path' is a file. Provide a folder." } else { Write-Host "The path '$Path' is a file. Please provide a folder path." -ForegroundColor Red Write-Host "→ Update-MyCorpTests" -ForegroundColor Green } return } if (-not (Test-Path -Path $Path -PathType Container)) { if ($NonInteractive.IsPresent) { Write-Error -Message "The path '$Path' does not exist." } else { Write-Host "The path '$Path' does not exist." -ForegroundColor Red Write-Host "→ Update-MyCorpTests" -ForegroundColor Green } return } # Check for test files, handle Get-ChildItem errors silently and test by Count try { $testFiles = Get-ChildItem -Path (Join-Path -Path $Path -ChildPath '*.Tests.ps1') -Recurse -ErrorAction SilentlyContinue } catch { $testFiles = @() } if (-not $testFiles -or $testFiles.Count -eq 0) { if ($NonInteractive.IsPresent) { Write-Error -Message "No test files found in the path '$Path'." } else { Write-Host "No test files found in the path '$Path'." -ForegroundColor Red Write-Host "→ Update-MyCorpTests" -ForegroundColor Green } return } # ----------------------- # Drift root handling # ----------------------- if ($DriftRoot) { try { $resolved = (Resolve-Path -Path $DriftRoot -ErrorAction SilentlyContinue).Path if (-not (Test-Path -Path $resolved)) { Write-Warning "The specified drift root directory '$DriftRoot' does not exist." } else { Set-Item -Path Env:\MyCorp_FOLDER_DRIFT -Value $resolved -ErrorAction SilentlyContinue Write-Verbose "Drift root directory set to: $resolved" } } catch { Write-Verbose "Drift root handling failed: $_" } } # ----------------------- # Run tests # ----------------------- Set-MtProgressView Write-MtProgress -Activity "Starting MyCorp" -Status "Reading MyCorp config..." -Force try { $__MyCorpSession.MyCorpConfig = Get-MtMyCorpConfig -Path $Path -ErrorAction SilentlyContinue } catch { Write-Verbose "Failed to read configuration via Get-MtMyCorpConfig: $_" } Write-MtProgress -Activity "Starting MyCorp" -Status "Discovering tests to run..." -Force $pesterResults = $null try { $pesterResults = Invoke-Pester -Configuration $pesterConfig } catch { Write-Error "Invoke-Pester failed: $_" Reset-MtProgressView return } if ($pesterResults) { Write-MtProgress -Activity "Processing test results" -Status "$($pesterResults.TotalCount) test(s)" -Force # Convert/present results try { $mycorpResults = ConvertTo-MtMyCorpResult -PesterResults $pesterResults -ErrorAction SilentlyContinue } catch { Write-Verbose "ConvertTo-MtMyCorpResult failed: $_" $mycorpResults = $null } if ($mycorpResults -and -not [string]::IsNullOrEmpty($out.OutputJsonFile)) { try { $mycorpResults | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue | Out-File -FilePath $out.OutputJsonFile -Encoding UTF8 } catch {} } if ($mycorpResults -and -not [string]::IsNullOrEmpty($out.OutputMarkdownFile)) { try { Write-MtProgress -Activity "Creating markdown report" $output = Get-MtMarkdownReport -MtMyCorpResults $mycorpResults -ErrorAction SilentlyContinue $output | Out-File -FilePath $out.OutputMarkdownFile -Encoding UTF8 } catch {} } if ($mycorpResults -and -not [string]::IsNullOrEmpty($out.OutputCsvFile)) { try { Write-MtProgress -Activity "Creating CSV" Convert-MtResultsToFlatObject -InputObject $mycorpResults -CsvFilePath $out.OutputCsvFile -ErrorAction SilentlyContinue } catch {} } if ($mycorpResults -and -not [string]::IsNullOrEmpty($out.OutputExcelFile)) { try { Write-MtProgress -Activity "Creating Excel workbook" Convert-MtResultsToFlatObject -InputObject $mycorpResults -ExcelFilePath $out.OutputExcelFile -ErrorAction SilentlyContinue } catch {} } if ($mycorpResults -and -not [string]::IsNullOrEmpty($out.OutputHtmlFile)) { try { Write-MtProgress -Activity "Creating html report" $output = Get-MtHtmlReport -MtMyCorpResults $mycorpResults -ErrorAction SilentlyContinue $output | Out-File -FilePath $out.OutputHtmlFile -Encoding UTF8 if (-not $NonInteractive.IsPresent) { Write-Host "MyCorp test report generated at $($out.OutputHtmlFile)" -ForegroundColor Green } if ((Get-MtUserInteractive) -and (-not $NonInteractive)) { Invoke-Item $out.OutputHtmlFile | Out-Null } } catch {} } # Notifications if ($mycorpResults -and $MailRecipient) { try { Write-MtProgress -Activity "Sending mail" Send-MtMail -MtMyCorpResults $mycorpResults -Recipient $MailRecipient -TestResultsUri $MailTestResultsUri -UserId $MailUserId -ErrorAction SilentlyContinue } catch {} } if ($mycorpResults -and $TeamId -and $TeamChannelId) { try { Write-MtProgress -Activity "Sending Teams message" Send-MtTeamsMessage -MtMyCorpResults $mycorpResults -TeamId $TeamId -TeamChannelId $TeamChannelId -TestResultsUri $MailTestResultsUri -ErrorAction SilentlyContinue } catch {} } if ($mycorpResults -and $TeamChannelWebhookUri) { try { Write-MtProgress -Activity "Sending Teams message" Send-MtTeamsMessage -MtMyCorpResults $mycorpResults -TeamChannelWebhookUri $TeamChannelWebhookUri -TestResultsUri $MailTestResultsUri -ErrorAction SilentlyContinue } catch {} } if ($Verbosity -eq 'None') { Write-Host "`nTests Passed: $($pesterResults.PassedCount), Failed: $($pesterResults.FailedCount), Skipped: $($pesterResults.SkippedCount)`n" } if (-not $SkipVersionCheck -and $version -and $version -ne 'Next') { try { Get-IsNewMyCorpVersionAvailable | Out-Null } catch {} } Write-MtProgress -Activity "Completed tests" -Status "Total $($pesterResults.TotalCount)" -Completed -Force } Reset-MtProgressView if ($PassThru) { return $mycorpResults } } # End function Invoke-MyCorp |