Files
ModernAdminStudio/Tests/BatchPipeline.Tests.ps1
T

346 lines
16 KiB
PowerShell

# 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
}
}
}