# Tests\BatchPipeline.Tests.ps1 # End-to-End Batch Pipeline & Headless UI Integration Tests BeforeAll { $scriptDir = Split-Path -Parent $PSScriptRoot # Load WPF / .NET GUI Assemblies for Headless verification Add-Type -AssemblyName PresentationFramework Add-Type -AssemblyName PresentationCore Add-Type -AssemblyName WindowsBase Add-Type -AssemblyName System.Data Add-Type -AssemblyName System.DirectoryServices # Import modules . (Join-Path $scriptDir "Modules\ConfigService.ps1") . (Join-Path $scriptDir "Modules\ADService.ps1") . (Join-Path $scriptDir "Modules\NetAppService.ps1") . (Join-Path $scriptDir "Modules\SqlService.ps1") . (Join-Path $scriptDir "Modules\ShareSyncService.ps1") . (Join-Path $scriptDir "Modules\UserResolverService.ps1") # Helper function matching Start-App.ps1 function Load-XamlElementTest { param([string]$Path) $raw = [System.IO.File]::ReadAllText($Path, [System.Text.Encoding]::UTF8) $clean = $raw -replace 'x:Class="[^"]*"', '' $stringReader = New-Object System.IO.StringReader($clean) $xmlReader = [System.Xml.XmlReader]::Create($stringReader) try { return [System.Windows.Markup.XamlReader]::Load($xmlReader) } finally { $xmlReader.Close() $stringReader.Close() } } # Initialize WPF Application & Theme Resource Dictionary for XAML parsing if ($null -eq [System.Windows.Application]::Current) { $app = New-Object System.Windows.Application } else { $app = [System.Windows.Application]::Current } $themePath = Join-Path $scriptDir "Styles\Theme.xaml" if (Test-Path $themePath) { $themeResource = Load-XamlElementTest -Path $themePath $app.Resources.MergedDictionaries.Add($themeResource) } } Describe "End-to-End Batch Pipeline & Headless UI Tests" -Tag "Mock", "Unit" { Context "Full 5-Stage Infrastructure Batch Pipeline Headless Execution" { It "executes all 5 transaction stages atomically with thread-safe logging" { $cfg = Get-AppConfig $tokens = @("gdhong", "cskim", "yylee") $shareName = "Finance_Share" $isAdd = $true $syncAd = $true $directDc = $true $bgSync = $true $quotaActive = $true $targetQtree = "qtree_finance_docs" $softLimit = 150 $hardLimit = 200 $reason = "2026 Q4 프로젝트 신규 참여자 권한 및 스토리지 일괄 적용" $ticket = "SR-20261022-99" $syncState = [hashtable]::Synchronized(@{ Logs = [System.Collections.Generic.List[string]]::new() Status = "작업 착수 중..." Finished = $false Error = $null TargetDc = "" CompletedCount = 0 }) # Stage 1: Active DC Discovery $activeDcInfo = Get-NetAppActiveDc -Config $cfg $activeDcInfo.ActiveDC | Should -Not -BeNullOrEmpty $targetDc = if ($directDc) { $activeDcInfo.ActiveDC } else { $cfg.ActiveDirectory.DomainController } $syncState.TargetDc = $targetDc $syncState.Logs.Add("[단계 1] NetApp Active DC 감지: $targetDc (Node: $($activeDcInfo.Node))") # Stage 2: Direct AD Security Group Creation on Target DC if ($syncAd) { $grpRes = New-ADSecurityGroupOnTargetDc -GroupName $shareName -GroupScope "Global" -TargetDc $targetDc -Members $tokens -Description "$reason (Ticket: $ticket)" -Config $cfg $grpRes.Success | Should -BeTrue $grpRes.TargetDc | Should -Be $targetDc $grpRes.AddedMembers.Count | Should -Be 3 $syncState.Logs.Add("[단계 2] AD 보안그룹 처리: $($grpRes.Message)") } # Stage 3: Background Replication to Remaining DCs if ($bgSync) { $allDcs = Get-ADDomainControllers -Config $cfg $otherDcs = @($allDcs | Where-Object { $_.HostName -ne $targetDc }) $otherDcs.Count | Should -Be 5 $replicatedDcs = [System.Collections.Generic.List[string]]::new() foreach ($odc in $otherDcs) { $syncRes = Sync-ADObjectToTargetDc -ObjectDN "CN=$shareName,OU=SecurityGroups,DC=corp,DC=local" -SourceDc $targetDc -DestinationDc $odc.HostName -Config $cfg $syncRes.Success | Should -BeTrue $replicatedDcs.Add($odc.HostName) } $replicatedDcs.Count | Should -Be 5 $syncState.Logs.Add("[단계 3] 6대 DC 복제 동기화: NetApp 활성 DC($targetDc) -> 5대 DC 비동기 복제 완료") } # Stage 4: NetApp CIFS Share ACL Binding with Smart Retry Corridor $permAction = if ($isAdd) { "grant" } else { "revoke" } $aclRes = Set-NetAppShareAcl -ShareName $shareName -UserOrGroup $shareName -Permission "change" -ActionType $permAction -MaxRetries 3 -RetryDelayMs 100 -Config $cfg $aclRes.Success | Should -BeTrue $syncState.Logs.Add("[단계 4] NetApp CIFS 공유 ACL: $($aclRes.Message)") # Stage 5: NetApp Quota Adjustment & DB Audit if ($quotaActive) { $qRes = Set-NetAppQuotaRule -Volume "vol_cifs_share01" -Qtree $targetQtree -SoftLimitGb ([int64]$softLimit) -HardLimitGb ([int64]$hardLimit) -Config $cfg $qRes.Success | Should -BeTrue $syncState.Logs.Add("[단계 5] NetApp Quota 조정: $($qRes.Message)") } $syncState.CompletedCount = $tokens.Count $syncState.Finished = $true $syncState.Status = "일괄 작업 성공 완료 (커밋됨 - 복제 대기 0초 달성)" # Validations $syncState.Finished | Should -BeTrue $syncState.Error | Should -BeNullOrEmpty $syncState.CompletedCount | Should -Be 3 $syncState.Logs.Count | Should -BeGreaterOrEqual 5 $syncState.Status | Should -Match "일괄 작업 성공 완료" } It "runs dry-run simulation generating step-by-step transaction forecast without altering state" { $tokens = @("gdhong", "cskim") $shareName = "DevOps_Share" $isAdd = $true $syncAd = $true $directDc = $true $bgDcSync = $true $quotaActive = $true $txtQtree = "qtree_dev_workspace" $txtSoft = "200" $txtHard = "250" $reason = "DevOps 프로젝트 그룹 할당" $ticket = "SR-2026-DRY" $simList = [System.Collections.Generic.List[PSCustomObject]]::new() $step = 1 # Step 1: User Verification foreach ($t in $tokens) { $simList.Add([PSCustomObject]@{ StepNo = $step++ TargetSystem = "Active Directory" ActionItem = "계정 검증" TargetObject = $t ValidationResult = "√ 유효 계정" }) } # Step 2: Multi-DC Discovery $targetDcName = "dc04.corp.local" if ($directDc) { $activeDcInfo = Get-NetAppActiveDc $targetDcName = $activeDcInfo.ActiveDC $simList.Add([PSCustomObject]@{ StepNo = $step++ TargetSystem = "Multi-DC 감지" ActionItem = "NetApp Active DC 바인딩 식별" TargetObject = "$targetDcName ($($activeDcInfo.Node))" ValidationResult = "√ 6대 DC 복제 지연 사전 차단" }) } # Step 3: AD Group Binding & Background Sync if ($syncAd) { foreach ($t in $tokens) { $simList.Add([PSCustomObject]@{ StepNo = $step++ TargetSystem = "Active Directory (직접)" ActionItem = if ($isAdd) { "보안그룹 멤버십 추가 ($reason)" } else { "보안그룹 멤버십 해제 ($reason)" } TargetObject = "$shareName (on $targetDcName)" ValidationResult = "√ Direct LDAP 바인딩" }) } if ($bgDcSync) { $simList.Add([PSCustomObject]@{ StepNo = $step++ TargetSystem = "Active Directory (Sync)" ActionItem = "타 도메인 컨트롤러 복제 큐잉" TargetObject = "dc01, dc02, dc03, dc05, dc06" ValidationResult = "√ 백그라운드 복제 큐 등록" }) } } # Step 4: NetApp ACL & Quota $simList.Add([PSCustomObject]@{ StepNo = $step++ TargetSystem = "NetApp ONTAP" ActionItem = "CIFS Share ACL" TargetObject = $shareName ValidationResult = "√ SID 즉시 식별 보장" }) if ($quotaActive) { $simList.Add([PSCustomObject]@{ StepNo = $step++ TargetSystem = "NetApp ONTAP" ActionItem = "Quota 용량 한도 변경 (Soft: ${txtSoft}GB, Hard: ${txtHard}GB)" TargetObject = $txtQtree ValidationResult = "√ 볼륨 여유 공간 충분" }) } # Step 5: Audit Log $simList.Add([PSCustomObject]@{ StepNo = $step++ TargetSystem = "MS SQL Server" ActionItem = "감사 로그 ($ticket)" TargetObject = "tbl_AuditLogs" ValidationResult = "√ 스키마 유효" }) $simList.Count | Should -Be 9 @($simList | Where-Object { $_.ValidationResult -like "√*" }).Count | Should -Be 9 } } Context "Headless UI & XAML Integrity Verification" { It "loads MainWindow.xaml and core controls successfully" { $mainPath = Join-Path $scriptDir "MainWindow.xaml" Test-Path $mainPath | Should -BeTrue $window = Load-XamlElementTest -Path $mainPath $window | Should -Not -BeNullOrEmpty $window.FindName("MainContentHost") | Should -Not -BeNullOrEmpty $window.FindName("TxtHeaderTitle") | Should -Not -BeNullOrEmpty $window.FindName("NavDashboard") | Should -Not -BeNullOrEmpty $window.FindName("NavAD") | Should -Not -BeNullOrEmpty $window.FindName("NavNetApp") | Should -Not -BeNullOrEmpty $window.FindName("NavShareAccess") | Should -Not -BeNullOrEmpty $window.FindName("NavBatchWs") | Should -Not -BeNullOrEmpty $window.FindName("NavSql") | Should -Not -BeNullOrEmpty $window.FindName("NavSettings") | Should -Not -BeNullOrEmpty } It "loads BatchWorkstationView.xaml and binds all interactive elements" { $viewPath = Join-Path $scriptDir "Views\BatchWorkstationView.xaml" Test-Path $viewPath | Should -BeTrue $view = Load-XamlElementTest -Path $viewPath $view | Should -Not -BeNullOrEmpty $view.FindName("TxtWsUserTokens") | Should -Not -BeNullOrEmpty $view.FindName("BtnLaunchResolver") | Should -Not -BeNullOrEmpty $view.FindName("BtnQuickCheckUsers") | Should -Not -BeNullOrEmpty $view.FindName("CmbWsShare") | Should -Not -BeNullOrEmpty $view.FindName("BtnDetectActiveDc") | Should -Not -BeNullOrEmpty $view.FindName("ChkDirectActiveDc") | Should -Not -BeNullOrEmpty $view.FindName("ChkBgDcSync") | Should -Not -BeNullOrEmpty $view.FindName("ChkWsEnableQuota") | Should -Not -BeNullOrEmpty $view.FindName("BtnRunDryRun") | Should -Not -BeNullOrEmpty $view.FindName("BtnApplyBatch") | Should -Not -BeNullOrEmpty $view.FindName("BtnResetWorkstation") | Should -Not -BeNullOrEmpty $view.FindName("GridSimulationResults") | Should -Not -BeNullOrEmpty $view.FindName("TxtWsConsoleLog") | Should -Not -BeNullOrEmpty $view.FindName("TxtWsExecutionStatus") | Should -Not -BeNullOrEmpty } It "loads UserResolverDialog.xaml and binds modal elements" { $dialogPath = Join-Path $scriptDir "Views\UserResolverDialog.xaml" Test-Path $dialogPath | Should -BeTrue $dialog = Load-XamlElementTest -Path $dialogPath $dialog | Should -Not -BeNullOrEmpty $dialog.FindName("TxtMultiInput") | Should -Not -BeNullOrEmpty $dialog.FindName("BtnResolveUsers") | Should -Not -BeNullOrEmpty $dialog.FindName("GridResolvedUsers") | Should -Not -BeNullOrEmpty $dialog.FindName("TxtResolutionSummary") | Should -Not -BeNullOrEmpty $dialog.FindName("BadgeSummary") | Should -Not -BeNullOrEmpty $dialog.FindName("BtnSelectResolvedOnly") | Should -Not -BeNullOrEmpty $dialog.FindName("BtnSelectAllDialog") | Should -Not -BeNullOrEmpty $dialog.FindName("BtnDeselectAllDialog") | Should -Not -BeNullOrEmpty $dialog.FindName("BtnDialogConfirm") | Should -Not -BeNullOrEmpty $dialog.FindName("BtnDialogCancel") | Should -Not -BeNullOrEmpty } It "loads all remaining Views without XAML parsing errors" { $views = @("DashboardView.xaml", "ActiveDirectoryView.xaml", "NetAppView.xaml", "SqlDatabaseView.xaml", "SettingsView.xaml", "ShareAccessManagerView.xaml") foreach ($vName in $views) { $vPath = Join-Path $scriptDir "Views\$vName" Test-Path $vPath | Should -BeTrue $vObj = Load-XamlElementTest -Path $vPath $vObj | Should -Not -BeNullOrEmpty } } } Context "Event Handler Variable Scopes & String Formatting" { It "formats Korean summary string without null evaluation for normCount, ambCount, and notFound" { $normCount = 3 $ambCount = 2 $notFound = 1 $summary = "정상 매칭: $($normCount)건 | 동명이인: $($ambCount)건 | 미발견: $($notFound)건" $summary | Should -Be "정상 매칭: 3건 | 동명이인: 2건 | 미발견: 1건" $summary | Should -Not -Match ":\s*건" } It "formats QuickCheck Korean summary string for amb count accurately" { $amb = 2 $msg = "입력된 사용자 중 동명이인이 $($amb)건 발견되었습니다." $msg | Should -Be "입력된 사용자 중 동명이인이 2건 발견되었습니다." $msg | Should -Not -Match "동명이인이\s*건" } } Context "Headless Modal Dialog & ShowDialog Execution Integrity" { It "executes Show-UserResolverDialog with ShowDialog() without GUI errors" { . "$scriptDir\Start-App.ps1" -NoShow $resolved = Show-UserResolverDialog -InitialText "gdhong, swjung" -AutoConfirm $resolved | Should -Not -BeNullOrEmpty $resolved.Count | Should -Be 2 @($resolved.SamAccountName) | Should -Contain "gdhong" @($resolved.SamAccountName) | Should -Contain "swjung" } It "navigates all application pages via Navigate-ToPage without UI errors" { . "$scriptDir\Start-App.ps1" -NoShow $pages = @("Dashboard", "AD", "NetApp", "ShareAccess", "BatchWs", "Sql", "Settings") foreach ($p in $pages) { { Navigate-ToPage -PageName $p } | Should -Not -Throw } } } Context "Headless Application Bootstrapping" { It "executes Start-App.ps1 with -NoShow parameter exiting cleanly with code 0" { $psExe = if ($PSVersionTable.PSEdition -eq "Core") { "pwsh" } else { "powershell" } $proc = Start-Process -FilePath $psExe -ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "$scriptDir\Start-App.ps1", "-NoShow" -PassThru -Wait -WindowStyle Hidden $proc.ExitCode | Should -Be 0 } } }