[CmdletBinding()] param() $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest $repositoryRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) $temporaryRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()) $testDirectory = Join-Path $temporaryRoot ("eis-migration-smoke-{0}" -f [guid]::NewGuid().ToString('N')) $nodeProcess = $null $dotnetProcess = $null function Get-AvailableTcpPort { $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0) try { $listener.Start() return ([System.Net.IPEndPoint] $listener.LocalEndpoint).Port } finally { $listener.Stop() } } function Start-TestProcess { param( [Parameter(Mandatory)] [string] $FileName, [Parameter(Mandatory)] [string[]] $ArgumentList, [Parameter(Mandatory)] [hashtable] $Environment ) $startInfo = [System.Diagnostics.ProcessStartInfo]::new() $startInfo.FileName = $FileName $startInfo.WorkingDirectory = $repositoryRoot $startInfo.UseShellExecute = $false $startInfo.CreateNoWindow = $true $startInfo.RedirectStandardOutput = $true $startInfo.RedirectStandardError = $true foreach ($argument in $ArgumentList) { $startInfo.ArgumentList.Add($argument) } foreach ($entry in $Environment.GetEnumerator()) { $startInfo.Environment[$entry.Key] = [string] $entry.Value } return [System.Diagnostics.Process]::Start($startInfo) } function Wait-ForUrl { param( [Parameter(Mandatory)] [uri] $Uri, [Parameter(Mandatory)] [System.Diagnostics.Process[]] $Processes ) $deadline = [DateTimeOffset]::UtcNow.AddSeconds(25) while ([DateTimeOffset]::UtcNow -lt $deadline) { foreach ($process in $Processes) { if ($process.HasExited) { $standardOutput = $process.StandardOutput.ReadToEnd() $standardError = $process.StandardError.ReadToEnd() throw "Process $($process.Id) exited with code $($process.ExitCode) before $Uri became ready`n$standardOutput`n$standardError" } } try { $response = Invoke-WebRequest -Uri $Uri -TimeoutSec 2 if ($response.StatusCode -eq 200) { return } } catch { Start-Sleep -Milliseconds 250 } } throw "Timed out waiting for $Uri" } try { New-Item -ItemType Directory -Path $testDirectory | Out-Null $legacyPort = Get-AvailableTcpPort $webPort = Get-AvailableTcpPort $legacyBaseUrl = "http://127.0.0.1:$legacyPort" $webBaseUrl = "http://127.0.0.1:$webPort" $nodeExecutable = (Get-Command node.exe -ErrorAction Stop).Source $dotnetExecutable = (Get-Command dotnet.exe -ErrorAction Stop).Source $nodeProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @('server.mjs') -Environment @{ NODE_ENV = 'test' HOST = '127.0.0.1' PORT = [string] $legacyPort DATABASE_CLIENT = 'sqlite' SQLITE_PATH = (Join-Path $testDirectory 'smoke.sqlite') INITIAL_ADMIN_USERNAME = 'migration_admin' INITIAL_ADMIN_PASSWORD = 'Migration123!' INITIAL_ADMIN_DISPLAY_NAME = '迁移测试管理员' REDIS_URL = '' REDIS_SESSION_URL = '' TOTP_ENCRYPTION_KEY = 'migration-smoke-totp-key-32-characters-minimum' DOCUMENT_VERIFICATION_SECRET = 'migration-smoke-document-key-32-characters-minimum' } $projectPath = Join-Path $repositoryRoot 'src\Eis.Web\Eis.Web.csproj' $dotnetProcess = Start-TestProcess -FileName $dotnetExecutable -ArgumentList @( 'run', '--project', $projectPath, '--configuration', 'Release', '--no-build', '--no-launch-profile', '--', '--urls', $webBaseUrl ) -Environment @{ ASPNETCORE_ENVIRONMENT = 'Development' LegacyNode__Enabled = 'true' LegacyNode__BaseUrl = $legacyBaseUrl } Wait-ForUrl -Uri "$webBaseUrl/health/live" -Processes @($nodeProcess, $dotnetProcess) Wait-ForUrl -Uri "$webBaseUrl/health/migration" -Processes @($nodeProcess, $dotnetProcess) $index = Invoke-WebRequest -Uri "$webBaseUrl/" if ($index.Content -notmatch '衡准') { throw 'ASP.NET Core did not serve the existing index.html' } $clientModule = Invoke-WebRequest -Uri "$webBaseUrl/src/client/api.mjs" if ($clientModule.Content -notmatch 'export function api') { throw 'ASP.NET Core did not serve the existing client module' } $homePayload = Invoke-RestMethod -Uri "$webBaseUrl/api/public/home" if ($homePayload.ok -ne $true) { throw 'Legacy public API proxy did not preserve the JSON response' } $session = [Microsoft.PowerShell.Commands.WebRequestSession]::new() $loginBody = @{ username = 'migration_admin'; password = 'Migration123!' } | ConvertTo-Json -Compress $login = Invoke-RestMethod -Uri "$webBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $loginBody -WebSession $session if ($login.ok -ne $true -or $login.user.username -ne 'migration_admin') { throw 'Legacy login API proxy did not preserve the response' } $currentUser = Invoke-RestMethod -Uri "$webBaseUrl/api/auth/me" -WebSession $session if ($currentUser.user.username -ne 'migration_admin') { throw 'Legacy API proxy did not preserve the session cookie' } [pscustomobject]@{ AspNetCoreHost = 'passed' StaticAssets = 'passed' JsonProxy = 'passed' SessionCookie = 'passed' } | Format-List } finally { foreach ($process in @($dotnetProcess, $nodeProcess)) { if ($null -ne $process -and -not $process.HasExited) { $process.Kill($true) $process.WaitForExit() } if ($null -ne $process) { $process.Dispose() } } if (Test-Path -LiteralPath $testDirectory) { $resolvedTestDirectory = [System.IO.Path]::GetFullPath($testDirectory) if (-not $resolvedTestDirectory.StartsWith($temporaryRoot, [StringComparison]::OrdinalIgnoreCase) -or -not ([System.IO.Path]::GetFileName($resolvedTestDirectory)).StartsWith('eis-migration-smoke-', [StringComparison]::Ordinal)) { throw "Refusing to remove unexpected smoke-test directory: $resolvedTestDirectory" } Remove-Item -LiteralPath $resolvedTestDirectory -Recurse -Force } }