# Tests\NetAppService.Tests.ps1 # Unit & Integration Tests for NetAppService Module BeforeAll { $scriptDir = Split-Path -Parent $PSScriptRoot . (Join-Path $scriptDir "Modules\NetAppService.ps1") } Describe "NetAppService Unit Tests" -Tag "Mock", "Unit" { Context "Resolve-NetAppParameters Precedence & Resolution" { It "prefers explicit parameters over config defaults" { $cfg = @{ NetApp = @{ ClusterEndpoint = "https://10.200.10.50" Username = "config_user" Password = "config_password" DefaultSvm = "svm_config" UseLive = $false IgnoreSslErrors = $true } } $resolved = Resolve-NetAppParameters -ClusterEndpoint "https://10.200.10.99" -Username "explicit_user" -SvmName "svm_explicit" -Config $cfg $resolved.Endpoint | Should -Be "https://10.200.10.99" $resolved.Username | Should -Be "explicit_user" $resolved.SvmName | Should -Be "svm_explicit" } It "falls back to config values when parameters are empty" { $cfg = @{ NetApp = @{ ClusterEndpoint = "https://10.200.10.50" Username = "config_admin" DefaultSvm = "svm_prod01" UseLive = $false IgnoreSslErrors = $true } } $resolved = Resolve-NetAppParameters -Config $cfg $resolved.Endpoint | Should -Be "https://10.200.10.50" $resolved.Username | Should -Be "config_admin" $resolved.SvmName | Should -Be "svm_prod01" $resolved.UseLive | Should -BeFalse } } Context "Mock Data Retrieval (Qtrees, Quotas, Shares)" { It "retrieves sample Qtrees with capacity and volume details" { $qtrees = Get-NetAppSampleQtrees $qtrees.Count | Should -BeGreaterThan 0 $first = $qtrees[0] $first.QtreeName | Should -Not -BeNullOrEmpty $first.Volume | Should -Not -BeNullOrEmpty $first.SecurityStyle | Should -Not -BeNullOrEmpty $first.IsLive | Should -BeFalse } It "filters sample Qtrees by Volume name" { $filtered = Get-NetAppSampleQtrees -Filter "vol_cifs_share01" $filtered.Count | Should -Be 2 foreach ($q in $filtered) { $q.Volume | Should -Be "vol_cifs_share01" } } It "retrieves sample Quota rules with Soft and Hard limits" { $quotas = Get-NetAppSampleQuotas $quotas.Count | Should -BeGreaterThan 0 foreach ($q in $quotas) { $q.VolumeQtree | Should -Not -BeNullOrEmpty $q.QuotaType | Should -Not -BeNullOrEmpty $q.Target | Should -Not -BeNullOrEmpty $q.HardLimit | Should -Not -BeNullOrEmpty } } It "retrieves sample CIFS shares with paths and permissions" { $shares = Get-NetAppSampleShares $shares.Count | Should -BeGreaterThan 0 foreach ($s in $shares) { $s.ShareName | Should -Not -BeNullOrEmpty $s.Path | Should -Not -BeNullOrEmpty $s.Comment | Should -Not -BeNullOrEmpty $s.AclSummary | Should -Not -BeNullOrEmpty } } } Context "Storage Action Operations (Demo Simulation Mode)" { It "simulates Qtree creation on specified volume" { $cfg = @{ NetApp = @{ UseLive = $false } } $res = New-NetAppQtreeAction -Volume "vol_cifs_share01" -QtreeName "qt_new_project" -SecurityStyle "ntfs" -Config $cfg $res.Success | Should -BeTrue $res.Message | Should -Match "qt_new_project" $res.Message | Should -Match "데모 모드" } It "simulates CIFS share creation with directory path" { $cfg = @{ NetApp = @{ UseLive = $false } } $res = New-NetAppShareAction -ShareName "Marketing_Share" -Path "/vol_cifs_share01/qt_marketing" -Comment "Marketing folder" -Config $cfg $res.Success | Should -BeTrue $res.Message | Should -Match "Marketing_Share" $res.Message | Should -Match "가상 생성" } It "simulates Quota policy adjustment with Soft and Hard limits" { $cfg = @{ NetApp = @{ UseLive = $false } } $res = Set-NetAppQuotaRule -Volume "vol_cifs_share01" -Qtree "qt_marketing" -SoftLimitGb 200 -HardLimitGb 250 -Config $cfg $res.Success | Should -BeTrue $res.Message | Should -Match "200GB" $res.Message | Should -Match "250GB" } } Context "Active DC Discovery: Edge Case Parsing (REST JSON Schema Variants)" { It "parses discovered_servers schema using 'server_name' property" { $mockRecord = [PSCustomObject]@{ records = @( [PSCustomObject]@{ svm = [PSCustomObject]@{ name = "svm_cifs_prod01" } discovered_servers = @( [PSCustomObject]@{ server_name = "dc04.corp.local" server_ip = "10.20.1.14" server_type = "dc" preference = "preferred" state = "ok" node = [PSCustomObject]@{ name = "node-01" } } ) } ) } Mock Invoke-NetAppRestRequest { return $mockRecord } $res = Get-NetAppActiveDc -ClusterEndpoint "https://fake-cluster" -SvmName "svm_cifs_prod01" -UseLive $true $res.ActiveDC | Should -Be "dc04.corp.local" $res.Node | Should -Be "node-01" $res.IsLive | Should -BeTrue } It "falls back to 'name' property when server_name is absent" { $mockRecord = [PSCustomObject]@{ records = @( [PSCustomObject]@{ svm = [PSCustomObject]@{ name = "svm_cifs_prod01" } discovered_servers = @( [PSCustomObject]@{ name = "dc02.corp.local" server_ip = "10.10.1.12" server_type = "dc" preference = "favored" state = "ok" } ) } ) } Mock Invoke-NetAppRestRequest { return $mockRecord } $res = Get-NetAppActiveDc -ClusterEndpoint "https://fake-cluster" -SvmName "svm_cifs_prod01" -UseLive $true $res.ActiveDC | Should -Be "dc02.corp.local" } It "falls back to 'server' property when both server_name and name are absent" { $mockRecord = [PSCustomObject]@{ records = @( [PSCustomObject]@{ svm = [PSCustomObject]@{ name = "svm_cifs_prod01" } discovered_servers = @( [PSCustomObject]@{ server = "dc03.corp.local" server_ip = "10.10.1.13" server_type = "dc" preference = "regular" state = "ok" } ) } ) } Mock Invoke-NetAppRestRequest { return $mockRecord } $res = Get-NetAppActiveDc -ClusterEndpoint "https://fake-cluster" -SvmName "svm_cifs_prod01" -UseLive $true $res.ActiveDC | Should -Be "dc03.corp.local" } It "falls back to 'server_ip' when no hostname is provided" { $mockRecord = [PSCustomObject]@{ records = @( [PSCustomObject]@{ svm = [PSCustomObject]@{ name = "svm_cifs_prod01" } discovered_servers = @( [PSCustomObject]@{ server_ip = "10.30.1.15" server_type = "dc" preference = "regular" state = "ok" } ) } ) } Mock Invoke-NetAppRestRequest { return $mockRecord } $res = Get-NetAppActiveDc -ClusterEndpoint "https://fake-cluster" -SvmName "svm_cifs_prod01" -UseLive $true $res.ActiveDC | Should -Be "10.30.1.15" } } Context "Active DC Discovery: Preference Sorting & State Filtering" { It "selects 'preferred' over 'favored' and 'regular' among OK servers" { $mockRecord = [PSCustomObject]@{ records = @( [PSCustomObject]@{ svm = [PSCustomObject]@{ name = "svm_cifs_prod01" } discovered_servers = @( [PSCustomObject]@{ server_name = "dc-regular.corp.local"; state = "ok"; server_type = "dc"; preference = "regular" }, [PSCustomObject]@{ server_name = "dc-preferred.corp.local"; state = "ok"; server_type = "dc"; preference = "preferred" }, [PSCustomObject]@{ server_name = "dc-favored.corp.local"; state = "ok"; server_type = "dc"; preference = "favored" } ) } ) } Mock Invoke-NetAppRestRequest { return $mockRecord } $res = Get-NetAppActiveDc -ClusterEndpoint "https://fake-cluster" -SvmName "svm_cifs_prod01" -UseLive $true $res.ActiveDC | Should -Be "dc-preferred.corp.local" $res.Preference | Should -Be "preferred" } It "discards down servers even if their preference is preferred" { $mockRecord = [PSCustomObject]@{ records = @( [PSCustomObject]@{ svm = [PSCustomObject]@{ name = "svm_cifs_prod01" } discovered_servers = @( [PSCustomObject]@{ server_name = "dc-down.corp.local"; state = "down"; server_type = "dc"; preference = "preferred" }, [PSCustomObject]@{ server_name = "dc-active.corp.local"; state = "ok"; server_type = "dc"; preference = "favored" } ) } ) } Mock Invoke-NetAppRestRequest { return $mockRecord } $res = Get-NetAppActiveDc -ClusterEndpoint "https://fake-cluster" -SvmName "svm_cifs_prod01" -UseLive $true $res.ActiveDC | Should -Be "dc-active.corp.local" $res.Preference | Should -Be "favored" } It "returns demo active DC dc04.corp.local when UseLive is false" { $cfg = @{ NetApp = @{ UseLive = $false } } $activeDc = Get-NetAppActiveDc -Config $cfg $activeDc.ActiveDC | Should -Be "dc04.corp.local" $activeDc.Node | Should -Be "ontap-node-02" $activeDc.Status | Should -Match "OK" $activeDc.IsLive | Should -BeFalse $activeDc.AllOkDcs | Should -Contain "dc04.corp.local" $activeDc.AllOkDcs | Should -Contain "dc01.corp.local" } It "falls back to demo DC when live REST endpoints fail" { Mock Invoke-NetAppRestRequest { throw "Network unreachable" } $res = Get-NetAppActiveDc -ClusterEndpoint "https://bad-cluster" -UseLive $true $res.ActiveDC | Should -Be "dc04.corp.local" $res.IsLive | Should -BeFalse $res.FallbackReason | Should -Match "Network unreachable" } } Context "CIFS Share ACL Binding with Smart Retry Corridor" { It "successfully applies share ACL in demo mode" { $cfg = @{ NetApp = @{ UseLive = $false } } $res = Set-NetAppShareAcl -ShareName "Finance_Share" -UserOrGroup "Finance_Share" -Permission "change" -ActionType "grant" -Config $cfg $res.Success | Should -BeTrue $res.Message | Should -Match "Finance_Share" $res.Message | Should -Match "데모 모드" } It "formats retry count correctly without Unicode variable expansion failure" { # Regression test for Korean interpolation bug where $MaxRetries회 evaluated to empty $attempt = 3 $maxRetries = 3 $lastError = "SID lookup timeout" $obj = [PSCustomObject]@{ Success = $false Message = "NetApp Share ACL 반영 실패 ($($maxRetries)회 재시도 초과): $lastError" Attempts = $attempt } $obj.Message | Should -Match "\(3회 재시도 초과\)" $obj.Message | Should -Not -Match "\(\s*회 재시도 초과\)" } } Context "Live Infrastructure Path & Timeout Handling" -Tag "Live" { It "gracefully fails over and reports unreachable endpoint in live mode" { $res = Test-NetAppConnection -ClusterEndpoint "https://127.0.0.1:65531" -TimeoutSec 1 $res.Success | Should -BeFalse $res.Message | Should -Match "NetApp.*연결 실패" } } }