Excel:班级、账号、考生、缴费、考场、成绩、志愿、录取、报到及正式录取名册均改为 ClosedXML 原生处理,支持模板、校验、筛选和导入预检。[SystemWorkbook.cs](C:\\Users\\BI\\Documents\\EIS-dotnet\\src\\Eis.Infrastructure\\Spreadsheets\\SystemWorkbook.cs) 文书:录取通知书模板、成绩单/准考证/通知书业务数据与防伪验真已脱离 Node;保留现有前端排版导出效果。[AdminAdmissionService.Documents.cs](C:\\Users\\BI\\Documents\\EIS-dotnet\\src\\Eis.Infrastructure\\Administration\\AdminAdmissionService.Documents.cs) 缓存:完成 Redis 命名空间版本缓存、有界本地回退、请求合并和并发失效保护;公开数据及成绩写入后立即失效。[ApplicationCache.cs](C:\\Users\\BI\\Documents\\EIS-dotnet\\src\\Eis.Infrastructure\\Caching\\ApplicationCache.cs) 迁移清单已更新为完成。[MIGRATION.md](C:\\Users\\BI\\Documents\\EIS-dotnet\\MIGRATION.md)
1964 lines
131 KiB
PowerShell
1964 lines
131 KiB
PowerShell
[CmdletBinding()]
|
|
param()
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
Set-StrictMode -Version Latest
|
|
|
|
$repositoryRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
|
$smokeArtifactRoot = [System.IO.Path]::GetFullPath((Join-Path $repositoryRoot 'artifacts\smoke'))
|
|
$testDirectory = Join-Path $smokeArtifactRoot ("eis-migration-smoke-{0}" -f [guid]::NewGuid().ToString('N'))
|
|
$nodeProcess = $null
|
|
$dotnetProcess = $null
|
|
$nativeAuthProcess = $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"
|
|
}
|
|
|
|
function Assert-JsonEquivalent {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string] $Expected,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $Actual,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $Label
|
|
)
|
|
|
|
$expectedNode = [System.Text.Json.Nodes.JsonNode]::Parse($Expected)
|
|
$actualNode = [System.Text.Json.Nodes.JsonNode]::Parse($Actual)
|
|
if (-not [System.Text.Json.Nodes.JsonNode]::DeepEquals($expectedNode, $actualNode)) {
|
|
$difference = Find-JsonDifference -Expected $expectedNode -Actual $actualNode -Path '$'
|
|
throw "$Label JSON payload differs from the legacy API at $difference"
|
|
}
|
|
}
|
|
|
|
function Assert-CandidateResultsEquivalent {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string] $Expected,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $Actual
|
|
)
|
|
|
|
$expectedNode = [System.Text.Json.Nodes.JsonNode]::Parse($Expected)
|
|
$actualNode = [System.Text.Json.Nodes.JsonNode]::Parse($Actual)
|
|
foreach ($node in @($expectedNode, $actualNode)) {
|
|
foreach ($summary in $node['summaries'].AsArray()) {
|
|
$summary.AsObject().Remove('verificationQr') | Out-Null
|
|
}
|
|
}
|
|
if (-not [System.Text.Json.Nodes.JsonNode]::DeepEquals($expectedNode, $actualNode)) {
|
|
$difference = Find-JsonDifference -Expected $expectedNode -Actual $actualNode -Path '$'
|
|
throw "Candidate route 'results' JSON payload differs from the legacy API at $difference"
|
|
}
|
|
}
|
|
|
|
function Assert-CandidateAdmissionsEquivalent {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string] $Expected,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $Actual
|
|
)
|
|
|
|
$expectedNode = [System.Text.Json.Nodes.JsonNode]::Parse($Expected)
|
|
$actualNode = [System.Text.Json.Nodes.JsonNode]::Parse($Actual)
|
|
foreach ($node in @($expectedNode, $actualNode)) {
|
|
foreach ($admission in $node['admissions'].AsArray()) {
|
|
$admission.AsObject().Remove('noticeVerificationQr') | Out-Null
|
|
}
|
|
}
|
|
if (-not [System.Text.Json.Nodes.JsonNode]::DeepEquals($expectedNode, $actualNode)) {
|
|
$difference = Find-JsonDifference -Expected $expectedNode -Actual $actualNode -Path '$'
|
|
throw "Candidate route 'admissions' JSON payload differs from the legacy API at $difference"
|
|
}
|
|
}
|
|
|
|
function Assert-ArrangementPreviewEquivalent {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string] $Expected,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $Actual
|
|
)
|
|
|
|
$expectedNode = [System.Text.Json.Nodes.JsonNode]::Parse($Expected)
|
|
$actualNode = [System.Text.Json.Nodes.JsonNode]::Parse($Actual)
|
|
foreach ($node in @($expectedNode, $actualNode)) {
|
|
foreach ($sample in $node['samples'].AsArray()) {
|
|
$sample.AsObject().Remove('generatedAt') | Out-Null
|
|
}
|
|
}
|
|
if (-not [System.Text.Json.Nodes.JsonNode]::DeepEquals($expectedNode, $actualNode)) {
|
|
$difference = Find-JsonDifference -Expected $expectedNode -Actual $actualNode -Path '$'
|
|
throw "Arrangement preview JSON payload differs from the legacy API at $difference"
|
|
}
|
|
}
|
|
|
|
function Find-JsonDifference {
|
|
param(
|
|
[AllowNull()]
|
|
[System.Text.Json.Nodes.JsonNode] $Expected,
|
|
|
|
[AllowNull()]
|
|
[System.Text.Json.Nodes.JsonNode] $Actual,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $Path
|
|
)
|
|
|
|
if ($null -eq $Expected -or $null -eq $Actual) {
|
|
return "$Path (expected=$Expected, actual=$Actual)"
|
|
}
|
|
|
|
if ($Expected -is [System.Text.Json.Nodes.JsonObject] -and $Actual -is [System.Text.Json.Nodes.JsonObject]) {
|
|
foreach ($entry in $Expected) {
|
|
if (-not $Actual.ContainsKey($entry.Key)) {
|
|
return "$Path.$($entry.Key) (missing from actual)"
|
|
}
|
|
if (-not [System.Text.Json.Nodes.JsonNode]::DeepEquals($entry.Value, $Actual[$entry.Key])) {
|
|
return Find-JsonDifference -Expected $entry.Value -Actual $Actual[$entry.Key] -Path "$Path.$($entry.Key)"
|
|
}
|
|
}
|
|
foreach ($entry in $Actual) {
|
|
if (-not $Expected.ContainsKey($entry.Key)) {
|
|
return "$Path.$($entry.Key) (unexpected in actual)"
|
|
}
|
|
}
|
|
return "$Path (object values differ)"
|
|
}
|
|
|
|
if ($Expected -is [System.Text.Json.Nodes.JsonArray] -and $Actual -is [System.Text.Json.Nodes.JsonArray]) {
|
|
if ($Expected.Count -ne $Actual.Count) {
|
|
return "$Path.Count (expected=$($Expected.Count), actual=$($Actual.Count))"
|
|
}
|
|
for ($index = 0; $index -lt $Expected.Count; $index++) {
|
|
if (-not [System.Text.Json.Nodes.JsonNode]::DeepEquals($Expected[$index], $Actual[$index])) {
|
|
return Find-JsonDifference -Expected $Expected[$index] -Actual $Actual[$index] -Path "$Path[$index]"
|
|
}
|
|
}
|
|
return "$Path (array values differ)"
|
|
}
|
|
|
|
return "$Path (expected=$($Expected.ToJsonString()), actual=$($Actual.ToJsonString()))"
|
|
}
|
|
|
|
try {
|
|
New-Item -ItemType Directory -Path $testDirectory | Out-Null
|
|
|
|
$legacyPort = Get-AvailableTcpPort
|
|
$webPort = Get-AvailableTcpPort
|
|
$nativeAuthPort = Get-AvailableTcpPort
|
|
$legacyBaseUrl = "http://127.0.0.1:$legacyPort"
|
|
$webBaseUrl = "http://127.0.0.1:$webPort"
|
|
$nativeAuthBaseUrl = "http://127.0.0.1:$nativeAuthPort"
|
|
|
|
$nodeExecutable = (Get-Command node.exe -ErrorAction Stop).Source
|
|
$dotnetExecutable = (Get-Command dotnet.exe -ErrorAction Stop).Source
|
|
$smokeDatabasePath = Join-Path $testDirectory 'smoke.sqlite'
|
|
$nodeEnvironment = @{
|
|
NODE_ENV = 'test'
|
|
HOST = '127.0.0.1'
|
|
PORT = [string] $legacyPort
|
|
DATABASE_CLIENT = 'sqlite'
|
|
SQLITE_PATH = $smokeDatabasePath
|
|
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'
|
|
}
|
|
$seedProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @('scripts/import-test-data.mjs', '--sqlite') -Environment $nodeEnvironment
|
|
if (-not $seedProcess.WaitForExit(30000)) {
|
|
$seedProcess.Kill($true)
|
|
throw 'Timed out while preparing the migration smoke-test database'
|
|
}
|
|
$seedOutput = $seedProcess.StandardOutput.ReadToEnd()
|
|
$seedError = $seedProcess.StandardError.ReadToEnd()
|
|
if ($seedProcess.ExitCode -ne 0) {
|
|
throw "Failed to prepare the migration smoke-test database`n$seedOutput`n$seedError"
|
|
}
|
|
$seedProcess.Dispose()
|
|
|
|
$registrationSwitchProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @(
|
|
'tests/helpers/enable-self-registration.mjs', $smokeDatabasePath
|
|
) -Environment $nodeEnvironment
|
|
if (-not $registrationSwitchProcess.WaitForExit(10000)) {
|
|
$registrationSwitchProcess.Kill($true)
|
|
throw 'Timed out while enabling self-registration in the smoke-test database'
|
|
}
|
|
$registrationSwitchOutput = $registrationSwitchProcess.StandardOutput.ReadToEnd()
|
|
$registrationSwitchError = $registrationSwitchProcess.StandardError.ReadToEnd()
|
|
if ($registrationSwitchProcess.ExitCode -ne 0) {
|
|
throw "Could not enable self-registration in the smoke-test database`n$registrationSwitchOutput`n$registrationSwitchError"
|
|
}
|
|
$registrationSwitchProcess.Dispose()
|
|
|
|
$arrangementCapacityProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @(
|
|
'tests/helpers/prepare-admin-arrangement-smoke.mjs', $smokeDatabasePath
|
|
) -Environment $nodeEnvironment
|
|
if (-not $arrangementCapacityProcess.WaitForExit(10000)) {
|
|
$arrangementCapacityProcess.Kill($true)
|
|
throw 'Timed out while preparing arrangement capacity in the smoke-test database'
|
|
}
|
|
$arrangementCapacityOutput = $arrangementCapacityProcess.StandardOutput.ReadToEnd()
|
|
$arrangementCapacityError = $arrangementCapacityProcess.StandardError.ReadToEnd()
|
|
if ($arrangementCapacityProcess.ExitCode -ne 0) {
|
|
throw "Could not prepare arrangement capacity in the smoke-test database`n$arrangementCapacityOutput`n$arrangementCapacityError"
|
|
}
|
|
$arrangementCapacityProcess.Dispose()
|
|
|
|
$projectPath = Join-Path $repositoryRoot 'src\Eis.Web\Eis.Web.csproj'
|
|
$buildProcess = Start-TestProcess -FileName $dotnetExecutable -ArgumentList @(
|
|
'build', $projectPath,
|
|
'--configuration', 'Release'
|
|
) -Environment @{}
|
|
if (-not $buildProcess.WaitForExit(60000)) {
|
|
$buildProcess.Kill($true)
|
|
throw 'Timed out while building the ASP.NET Core migration host'
|
|
}
|
|
$buildOutput = $buildProcess.StandardOutput.ReadToEnd()
|
|
$buildError = $buildProcess.StandardError.ReadToEnd()
|
|
if ($buildProcess.ExitCode -ne 0) {
|
|
throw "Failed to build the ASP.NET Core migration host`n$buildOutput`n$buildError"
|
|
}
|
|
$buildProcess.Dispose()
|
|
|
|
$nodeProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @('server.mjs') -Environment $nodeEnvironment
|
|
$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
|
|
DATABASE_CLIENT = 'sqlite'
|
|
SQLITE_PATH = $smokeDatabasePath
|
|
DOCUMENT_VERIFICATION_SECRET = 'migration-smoke-document-key-32-characters-minimum'
|
|
}
|
|
|
|
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'
|
|
}
|
|
|
|
$homeResponse = Invoke-WebRequest -Uri "$webBaseUrl/api/public/home"
|
|
if ($homeResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Public home request did not use the native ASP.NET Core endpoint'
|
|
}
|
|
$homePayload = $homeResponse.Content | ConvertFrom-Json
|
|
if ($homePayload.ok -ne $true) {
|
|
throw 'Native public API did not return a valid JSON response'
|
|
}
|
|
$legacyHomePayload = Invoke-RestMethod -Uri "$legacyBaseUrl/api/public/home"
|
|
if ($homePayload.stats.candidates -ne $legacyHomePayload.stats.candidates -or
|
|
$homePayload.stats.exams -ne $legacyHomePayload.stats.exams -or
|
|
$homePayload.stats.registrations -ne $legacyHomePayload.stats.registrations -or
|
|
$homePayload.schools.Count -ne $legacyHomePayload.schools.Count -or
|
|
$homePayload.classes.Count -ne $legacyHomePayload.classes.Count -or
|
|
$homePayload.notices.Count -ne $legacyHomePayload.notices.Count) {
|
|
throw 'Native public home aggregates do not match the legacy API'
|
|
}
|
|
foreach ($legacyExam in $legacyHomePayload.exams) {
|
|
$nativeExam = $homePayload.exams | Where-Object id -eq $legacyExam.id | Select-Object -First 1
|
|
if ($null -eq $nativeExam -or $nativeExam.subjects.Count -ne $legacyExam.subjects.Count -or
|
|
$nativeExam.totalScore -ne $legacyExam.totalScore -or $nativeExam.registrationCount -ne $legacyExam.registrationCount) {
|
|
throw "Native public exam projection does not match the legacy API for $($legacyExam.id)"
|
|
}
|
|
}
|
|
|
|
$announcementResponse = Invoke-WebRequest -Uri "$webBaseUrl/api/public/announcements"
|
|
if ($announcementResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Public announcements request did not use the native ASP.NET Core endpoint'
|
|
}
|
|
$nativeAnnouncements = $announcementResponse.Content | ConvertFrom-Json
|
|
$legacyAnnouncements = Invoke-RestMethod -Uri "$legacyBaseUrl/api/public/announcements"
|
|
foreach ($section in @('plans', 'qualifications', 'admissions', 'cutoffs', 'reports')) {
|
|
$nativeIds = @($nativeAnnouncements.$section | ForEach-Object id)
|
|
$legacyIds = @($legacyAnnouncements.$section | ForEach-Object id)
|
|
if (($nativeIds -join "`0") -ne ($legacyIds -join "`0")) {
|
|
throw "Native public announcement section '$section' does not match the legacy API"
|
|
}
|
|
$nativeSectionJson = $nativeAnnouncements.$section | ConvertTo-Json -Depth 100 -Compress
|
|
$legacySectionJson = $legacyAnnouncements.$section | ConvertTo-Json -Depth 100 -Compress
|
|
if ($nativeSectionJson -ne $legacySectionJson) {
|
|
throw "Native public announcement payload '$section' differs from the legacy API"
|
|
}
|
|
}
|
|
|
|
$session = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
$loginBody = @{ username = 'admin'; password = '12345678' } | 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 '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 'admin') {
|
|
throw 'Legacy API proxy did not preserve the session cookie'
|
|
}
|
|
|
|
$noticeBody = @{
|
|
title = 'ASP.NET Core 迁移冒烟通知'
|
|
content = '<p>原生公开读取验证</p><script>throw new Error("unsafe")</script>'
|
|
category = '系统测试'
|
|
pinned = $true
|
|
status = 'published'
|
|
} | ConvertTo-Json -Compress
|
|
$createdNotice = Invoke-RestMethod -Uri "$webBaseUrl/api/admin/notices" -Method Post -ContentType 'application/json' -Body $noticeBody -WebSession $session
|
|
$noticeResponse = Invoke-WebRequest -Uri "$webBaseUrl/api/public/notices/$($createdNotice.notice.id)"
|
|
if ($noticeResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Public notice request did not use the native ASP.NET Core endpoint'
|
|
}
|
|
$noticePayload = $noticeResponse.Content | ConvertFrom-Json
|
|
if ($noticePayload.notice.title -ne 'ASP.NET Core 迁移冒烟通知' -or $noticePayload.notice.content -match '<script') {
|
|
throw 'Native public notice endpoint did not preserve data or sanitize unsafe content'
|
|
}
|
|
|
|
$candidateSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
$candidateLoginBody = @{ username = '2026-HZ01-F-0001'; password = '12345678' } | ConvertTo-Json -Compress
|
|
$candidateLogin = Invoke-RestMethod -Uri "$webBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $candidateLoginBody -WebSession $candidateSession
|
|
if ($candidateLogin.user.username -ne '2026-HZ01-F-0001') {
|
|
throw 'Could not sign in as the migration verification candidate'
|
|
}
|
|
|
|
$candidateResults = Invoke-RestMethod -Uri "$webBaseUrl/api/candidate/results" -WebSession $candidateSession
|
|
$scoreCode = @($candidateResults.summaries | Where-Object verificationCode | Select-Object -First 1).verificationCode
|
|
if (-not $scoreCode) {
|
|
throw 'Seed data did not provide a score-report verification code'
|
|
}
|
|
$scoreVerificationResponse = Invoke-WebRequest -Uri "$webBaseUrl/api/public/verifications/$scoreCode"
|
|
if ($scoreVerificationResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Document verification request did not use the native ASP.NET Core endpoint'
|
|
}
|
|
$nativeScoreVerification = $scoreVerificationResponse.Content | ConvertFrom-Json
|
|
$legacyScoreVerification = Invoke-RestMethod -Uri "$legacyBaseUrl/api/public/verifications/$scoreCode"
|
|
if ($nativeScoreVerification.document.type -ne 'score-report' -or
|
|
$nativeScoreVerification.document.examName -ne $legacyScoreVerification.document.examName -or
|
|
$nativeScoreVerification.document.totalScore -ne $legacyScoreVerification.document.totalScore -or
|
|
$nativeScoreVerification.document.subjectCount -ne $legacyScoreVerification.document.subjectCount) {
|
|
throw 'Native score-report verification does not match the legacy API'
|
|
}
|
|
|
|
$admissionCodeProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @(
|
|
'tests/helpers/create-admission-verification.mjs', $smokeDatabasePath
|
|
) -Environment $nodeEnvironment
|
|
if (-not $admissionCodeProcess.WaitForExit(10000)) {
|
|
$admissionCodeProcess.Kill($true)
|
|
throw 'Timed out while reading an admission-notice verification code'
|
|
}
|
|
$noticeCode = $admissionCodeProcess.StandardOutput.ReadToEnd().Trim()
|
|
$admissionCodeError = $admissionCodeProcess.StandardError.ReadToEnd()
|
|
if ($admissionCodeProcess.ExitCode -ne 0) {
|
|
throw "Could not read an admission-notice verification code`n$admissionCodeError"
|
|
}
|
|
$admissionCodeProcess.Dispose()
|
|
if (-not $noticeCode) {
|
|
throw "Seed data did not provide an admission-notice verification code`n$admissionCodeError"
|
|
}
|
|
$nativeNoticeVerification = Invoke-RestMethod -Uri "$webBaseUrl/api/public/verifications/$noticeCode"
|
|
$legacyNoticeVerification = Invoke-RestMethod -Uri "$legacyBaseUrl/api/public/verifications/$noticeCode"
|
|
if ($nativeNoticeVerification.document.type -ne 'admission-notice' -or
|
|
$nativeNoticeVerification.document.noticeNumber -ne $legacyNoticeVerification.document.noticeNumber -or
|
|
$nativeNoticeVerification.document.schoolName -ne $legacyNoticeVerification.document.schoolName) {
|
|
throw 'Native admission-notice verification does not match the legacy API'
|
|
}
|
|
|
|
$invalidVerification = Invoke-WebRequest -Uri "$webBaseUrl/api/public/verifications/SR-000000000000000000000000" -SkipHttpErrorCheck
|
|
if ($invalidVerification.StatusCode -ne 404 -or $invalidVerification.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native document verification did not reject an invalid code'
|
|
}
|
|
|
|
$nativeAuthProcess = Start-TestProcess -FileName $dotnetExecutable -ArgumentList @(
|
|
'run',
|
|
'--project', $projectPath,
|
|
'--configuration', 'Release',
|
|
'--no-build',
|
|
'--no-launch-profile',
|
|
'--',
|
|
'--urls', $nativeAuthBaseUrl
|
|
) -Environment @{
|
|
ASPNETCORE_ENVIRONMENT = 'Development'
|
|
AUTH_NATIVE_ENABLED = 'true'
|
|
CANDIDATE_NATIVE_ENABLED = 'true'
|
|
CANDIDATE_NATIVE_ALLOW_MEMORY = 'true'
|
|
ADMIN_NATIVE_READS_ENABLED = 'true'
|
|
ADMIN_NATIVE_ORGANIZATION_WRITES_ENABLED = 'true'
|
|
ADMIN_NATIVE_ACCOUNT_BATCHES_ENABLED = 'true'
|
|
ADMIN_NATIVE_CONFIGURATION_ENABLED = 'true'
|
|
ADMIN_NATIVE_NOTICE_MANAGEMENT_ENABLED = 'true'
|
|
ADMIN_NATIVE_CENTERS_ENABLED = 'true'
|
|
ADMIN_NATIVE_OPERATIONAL_READS_ENABLED = 'true'
|
|
ADMIN_NATIVE_CANDIDATE_MANAGEMENT_ENABLED = 'true'
|
|
ADMIN_NATIVE_REGISTRATION_PAYMENT_WRITES_ENABLED = 'true'
|
|
ADMIN_NATIVE_WORKFLOW_OPERATIONS_ENABLED = 'true'
|
|
ADMIN_NATIVE_EXAM_MANAGEMENT_ENABLED = 'true'
|
|
ADMIN_NATIVE_ARRANGEMENTS_ENABLED = 'true'
|
|
ADMIN_NATIVE_RESULTS_ENABLED = 'true'
|
|
ADMIN_NATIVE_ADMISSIONS_ENABLED = 'true'
|
|
ADMIN_NATIVE_ALLOW_MEMORY = 'true'
|
|
LegacyNode__Enabled = 'true'
|
|
LegacyNode__BaseUrl = $legacyBaseUrl
|
|
DATABASE_CLIENT = 'sqlite'
|
|
SQLITE_PATH = $smokeDatabasePath
|
|
TOTP_ENCRYPTION_KEY = 'migration-smoke-totp-key-32-characters-minimum'
|
|
DOCUMENT_VERIFICATION_SECRET = 'migration-smoke-document-key-32-characters-minimum'
|
|
REDIS_URL = ''
|
|
REDIS_SESSION_URL = ''
|
|
}
|
|
Wait-ForUrl -Uri "$nativeAuthBaseUrl/health/live" -Processes @($nodeProcess, $nativeAuthProcess)
|
|
|
|
$anonymousCandidate = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/profile" -SkipHttpErrorCheck
|
|
if ($anonymousCandidate.StatusCode -ne 401 -or $anonymousCandidate.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native candidate API did not reject an anonymous request'
|
|
}
|
|
|
|
$nativeCandidateSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
$nativeCandidateLogin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $candidateLoginBody -WebSession $nativeCandidateSession
|
|
if ($nativeCandidateLogin.user.candidateNumber -ne '2026-HZ01-F-0001') {
|
|
throw 'Native authentication could not create the candidate parity-test session'
|
|
}
|
|
foreach ($candidateRoute in @('dashboard', 'notices', 'profile', 'exams', 'registrations')) {
|
|
$legacyCandidateResponse = Invoke-WebRequest -Uri "$legacyBaseUrl/api/candidate/$candidateRoute" -WebSession $candidateSession
|
|
$nativeCandidateResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/$candidateRoute" -WebSession $nativeCandidateSession
|
|
if ($nativeCandidateResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw "Candidate route '$candidateRoute' did not use the native ASP.NET Core endpoint"
|
|
}
|
|
Assert-JsonEquivalent -Expected $legacyCandidateResponse.Content -Actual $nativeCandidateResponse.Content -Label "Candidate route '$candidateRoute'"
|
|
}
|
|
$legacyCandidateResultsResponse = Invoke-WebRequest -Uri "$legacyBaseUrl/api/candidate/results" -WebSession $candidateSession
|
|
$nativeCandidateResultsResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/results" -WebSession $nativeCandidateSession
|
|
if ($nativeCandidateResultsResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw "Candidate route 'results' did not use the native ASP.NET Core endpoint"
|
|
}
|
|
Assert-CandidateResultsEquivalent -Expected $legacyCandidateResultsResponse.Content -Actual $nativeCandidateResultsResponse.Content
|
|
$nativeCandidateResults = $nativeCandidateResultsResponse.Content | ConvertFrom-Json
|
|
if (@($nativeCandidateResults.summaries | Where-Object { $_.verificationQr -match '^data:image/png;base64,' }).Count -eq 0) {
|
|
throw 'Native candidate results did not include a local PNG verification QR code'
|
|
}
|
|
$legacyCandidateAdmissionsResponse = Invoke-WebRequest -Uri "$legacyBaseUrl/api/candidate/admissions" -WebSession $candidateSession
|
|
$nativeCandidateAdmissionsResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/admissions" -WebSession $nativeCandidateSession
|
|
if ($nativeCandidateAdmissionsResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw "Candidate route 'admissions' did not use the native ASP.NET Core endpoint"
|
|
}
|
|
Assert-CandidateAdmissionsEquivalent -Expected $legacyCandidateAdmissionsResponse.Content -Actual $nativeCandidateAdmissionsResponse.Content
|
|
$admissionWriteProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @(
|
|
'tests/helpers/prepare-candidate-admission-write-smoke.mjs', $smokeDatabasePath
|
|
) -Environment $nodeEnvironment
|
|
if (-not $admissionWriteProcess.WaitForExit(10000)) {
|
|
$admissionWriteProcess.Kill($true)
|
|
throw 'Timed out while preparing the candidate admission preference smoke data'
|
|
}
|
|
$admissionWriteError = $admissionWriteProcess.StandardError.ReadToEnd()
|
|
if ($admissionWriteProcess.ExitCode -ne 0) {
|
|
throw "Could not prepare the candidate admission preference smoke data`n$admissionWriteError"
|
|
}
|
|
$admissionWriteProcess.Dispose()
|
|
$admissionContext = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/candidate/admissions" -WebSession $nativeCandidateSession
|
|
$admissionSetting = @($admissionContext.admissions | Where-Object { $_.status -eq 'filling' } | Select-Object -First 1)[0]
|
|
$admissionPlan = @($admissionSetting.plans | Select-Object -First 1)[0]
|
|
$admissionCategory = @($admissionPlan.categories | Where-Object { $_.preferenceTypes -contains 'general' } | Select-Object -First 1)[0]
|
|
if ($null -eq $admissionSetting -or $null -eq $admissionPlan -or $null -eq $admissionCategory) {
|
|
throw 'Native candidate admissions did not expose an eligible plan for preference submission'
|
|
}
|
|
$emptyPreference = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/admissions/$($admissionSetting.examId)/preferences" -Method Put -ContentType 'application/json' -Body '{"choices":[]}' -WebSession $nativeCandidateSession -SkipHttpErrorCheck
|
|
if ($emptyPreference.StatusCode -ne 400) {
|
|
throw 'Native admission preference API accepted an empty preference list'
|
|
}
|
|
$preferenceBody = @{
|
|
choices = @(@{
|
|
schoolId = $admissionPlan.schoolId
|
|
categoryCode = $admissionCategory.code
|
|
preferenceType = 'general'
|
|
})
|
|
} | ConvertTo-Json -Depth 5 -Compress
|
|
$firstPreferenceResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/admissions/$($admissionSetting.examId)/preferences" -Method Put -ContentType 'application/json' -Body $preferenceBody -WebSession $nativeCandidateSession
|
|
$firstPreference = $firstPreferenceResponse.Content | ConvertFrom-Json
|
|
if ($firstPreferenceResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $firstPreference.remainingSubmissions -ne 1 -or $firstPreference.locked -ne $false) {
|
|
throw 'Native admission preference API did not persist the first submission count'
|
|
}
|
|
$secondPreference = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/candidate/admissions/$($admissionSetting.examId)/preferences" -Method Put -ContentType 'application/json' -Body $preferenceBody -WebSession $nativeCandidateSession
|
|
if ($secondPreference.remainingSubmissions -ne 0 -or $secondPreference.locked -ne $true) {
|
|
throw 'Native admission preference API did not lock at the configured submission limit'
|
|
}
|
|
$lockedPreference = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/admissions/$($admissionSetting.examId)/preferences" -Method Put -ContentType 'application/json' -Body $preferenceBody -WebSession $nativeCandidateSession -SkipHttpErrorCheck
|
|
if ($lockedPreference.StatusCode -ne 409) {
|
|
throw 'Native admission preference API accepted a submission after automatic locking'
|
|
}
|
|
$legacyAdmissionsAfterWrite = Invoke-WebRequest -Uri "$legacyBaseUrl/api/candidate/admissions" -WebSession $candidateSession
|
|
$nativeAdmissionsAfterWrite = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/admissions" -WebSession $nativeCandidateSession
|
|
Assert-CandidateAdmissionsEquivalent -Expected $legacyAdmissionsAfterWrite.Content -Actual $nativeAdmissionsAfterWrite.Content
|
|
$appealResultId = $null
|
|
$appealExamId = $null
|
|
$appealOriginalScore = $null
|
|
$appealableResult = @($nativeCandidateResults.results | Where-Object { $null -eq $_.appeal } | Select-Object -First 1)[0]
|
|
if ($null -ne $appealableResult) {
|
|
$shortAppeal = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/results/$($appealableResult.id)/appeals" -Method Post -ContentType 'application/json' -Body '{"reason":"短"}' -WebSession $nativeCandidateSession -SkipHttpErrorCheck
|
|
if ($shortAppeal.StatusCode -ne 400 -or $shortAppeal.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native score appeal did not validate the minimum reason length'
|
|
}
|
|
$appealBody = @{ reason = '迁移冒烟验证:成绩与个人估分差异较大,请复核计分。' } | ConvertTo-Json -Compress
|
|
$appealResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/results/$($appealableResult.id)/appeals" -Method Post -ContentType 'application/json' -Body $appealBody -WebSession $nativeCandidateSession
|
|
$appeal = $appealResponse.Content | ConvertFrom-Json
|
|
if ($appealResponse.StatusCode -ne 201 -or $appealResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $appeal.workflow.status -ne 'pending') {
|
|
throw 'Native score appeal did not persist a pending workflow'
|
|
}
|
|
$appealResultId = $appealableResult.id
|
|
$appealExamId = $appealableResult.examId
|
|
$appealOriginalScore = $appealableResult.score
|
|
}
|
|
$admitProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @(
|
|
'tests/helpers/prepare-candidate-admit-smoke.mjs', $smokeDatabasePath, '2026-HZ01-F-0001'
|
|
) -Environment $nodeEnvironment
|
|
if (-not $admitProcess.WaitForExit(10000)) {
|
|
$admitProcess.Kill($true)
|
|
throw 'Timed out while preparing the candidate admit-card smoke data'
|
|
}
|
|
$admitRegistrationId = $admitProcess.StandardOutput.ReadToEnd().Trim()
|
|
$admitError = $admitProcess.StandardError.ReadToEnd()
|
|
if ($admitProcess.ExitCode -ne 0 -or -not $admitRegistrationId) {
|
|
throw "Could not prepare the candidate admit-card smoke data`n$admitError"
|
|
}
|
|
$admitProcess.Dispose()
|
|
$legacyAdmitResponse = Invoke-WebRequest -Uri "$legacyBaseUrl/api/candidate/registrations/$admitRegistrationId/admit-card" -WebSession $candidateSession
|
|
$nativeAdmitResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/registrations/$admitRegistrationId/admit-card" -WebSession $nativeCandidateSession
|
|
if ($nativeAdmitResponse.StatusCode -ne 200 -or
|
|
$nativeAdmitResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or
|
|
$nativeAdmitResponse.Headers['Content-Disposition'] -notmatch '^attachment;' -or
|
|
$nativeAdmitResponse.Content -notmatch '考试考场序号' -or
|
|
$nativeAdmitResponse.Content -ne $legacyAdmitResponse.Content) {
|
|
throw 'Native admit-card download did not match the legacy HTML document'
|
|
}
|
|
|
|
$registrationSchool = @($homePayload.schools | Select-Object -First 1)[0]
|
|
$registrationClass = @($homePayload.classes | Where-Object schoolId -eq $registrationSchool.id | Select-Object -First 1)[0]
|
|
if ($null -eq $registrationSchool -or $null -eq $registrationClass) {
|
|
throw 'Seed data did not provide a source school and class for native registration'
|
|
}
|
|
$registrationBody = @{
|
|
name = '原生迁移注册考生'
|
|
gender = '女'
|
|
password = 'Registration456!'
|
|
schoolId = $registrationSchool.id
|
|
classId = $registrationClass.id
|
|
} | ConvertTo-Json -Compress
|
|
$registrationResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/auth/register" -Method Post -ContentType 'application/json' -Body $registrationBody
|
|
if ($registrationResponse.StatusCode -ne 201 -or $registrationResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Self-registration request did not use the native ASP.NET Core endpoint'
|
|
}
|
|
$registration = $registrationResponse.Content | ConvertFrom-Json
|
|
$registeredLoginBody = @{ username = $registration.registrationNumber; password = 'Registration456!' } | ConvertTo-Json -Compress
|
|
$registeredSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
$registeredLogin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $registeredLoginBody -WebSession $registeredSession
|
|
if ($registeredLogin.user.candidateNumber -ne $registration.registrationNumber) {
|
|
throw 'Native self-registration did not create a usable candidate account'
|
|
}
|
|
$incompleteDashboard = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/dashboard" -WebSession $registeredSession -SkipHttpErrorCheck
|
|
if ($incompleteDashboard.StatusCode -ne 428) {
|
|
throw 'Native candidate API did not require completion of a newly registered profile'
|
|
}
|
|
$incompleteProfile = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/profile" -WebSession $registeredSession
|
|
if ($incompleteProfile.StatusCode -ne 200) {
|
|
throw 'Native candidate profile route was not available during onboarding'
|
|
}
|
|
|
|
$invalidSpecialtyBody = @{
|
|
name = '原生迁移注册考生'
|
|
gender = '女'
|
|
idNumber = 'SMOKE-INVALID-SPECIALTY'
|
|
phone = '13800000000'
|
|
email = 'candidate@example.test'
|
|
nativePlace = '江苏连云港'
|
|
address = '迁移测试路 1 号'
|
|
schoolId = $registrationSchool.id
|
|
classId = $registrationClass.id
|
|
provinceCode = '320000'
|
|
cityCode = '320700'
|
|
districtCode = '320706'
|
|
specialtyCategory = 'arts'
|
|
specialtyType = 'track_field'
|
|
} | ConvertTo-Json -Compress
|
|
$invalidSpecialty = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/profile" -Method Put -ContentType 'application/json' -Body $invalidSpecialtyBody -WebSession $registeredSession -SkipHttpErrorCheck
|
|
if ($invalidSpecialty.StatusCode -ne 400) {
|
|
throw 'Native candidate profile API accepted a mismatched specialty category and type'
|
|
}
|
|
|
|
$profileIdNumber = "SMOKE-NATIVE-$([guid]::NewGuid().ToString('N'))"
|
|
$profileBody = @{
|
|
name = '原生迁移注册考生'
|
|
gender = '女'
|
|
idNumber = $profileIdNumber
|
|
phone = '13800000000'
|
|
email = 'candidate@example.test'
|
|
address = '迁移测试路 1 号'
|
|
emergencyContact = '测试联系人'
|
|
emergencyPhone = '13900000000'
|
|
nativePlace = '江苏连云港'
|
|
birthDate = '2010-01-02'
|
|
ethnicity = '汉族'
|
|
postalCode = '222000'
|
|
guardianName = '测试监护人'
|
|
guardianPhone = '13700000000'
|
|
schoolId = $registrationSchool.id
|
|
classId = $registrationClass.id
|
|
provinceCode = '320000'
|
|
cityCode = '320700'
|
|
districtCode = '320706'
|
|
specialtyCategory = ''
|
|
specialtyType = ''
|
|
specialtyCertificate = ''
|
|
policyEligibility = ''
|
|
} | ConvertTo-Json -Compress
|
|
$profileUpdateResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/profile" -Method Put -ContentType 'application/json' -Body $profileBody -WebSession $registeredSession
|
|
if ($profileUpdateResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Candidate profile update did not use the native ASP.NET Core endpoint'
|
|
}
|
|
$profileUpdate = $profileUpdateResponse.Content | ConvertFrom-Json
|
|
if ($profileUpdate.profile.profileCompleted -ne $true -or $profileUpdate.profile.districtName -ne '海州区' -or $profileUpdate.profile.status -ne 'pending') {
|
|
throw 'Native candidate profile update did not resolve the region or persist onboarding state'
|
|
}
|
|
|
|
$prematureRegistrationBody = @{ examId = 'not-approved-yet'; subjectIds = @('none') } | ConvertTo-Json -Compress
|
|
$prematureRegistration = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/registrations" -Method Post -ContentType 'application/json' -Body $prematureRegistrationBody -WebSession $registeredSession -SkipHttpErrorCheck
|
|
if ($prematureRegistration.StatusCode -ne 403) {
|
|
throw 'Native registration API did not require an approved candidate profile'
|
|
}
|
|
|
|
$candidateWriteSetup = Start-TestProcess -FileName $nodeExecutable -ArgumentList @(
|
|
'tests/helpers/prepare-candidate-write-smoke.mjs', $smokeDatabasePath, $registration.registrationNumber
|
|
) -Environment $nodeEnvironment
|
|
if (-not $candidateWriteSetup.WaitForExit(10000)) {
|
|
$candidateWriteSetup.Kill($true)
|
|
throw 'Timed out while preparing the native candidate write smoke test'
|
|
}
|
|
$candidateWriteSetupOutput = $candidateWriteSetup.StandardOutput.ReadToEnd().Trim()
|
|
$candidateWriteSetupError = $candidateWriteSetup.StandardError.ReadToEnd()
|
|
if ($candidateWriteSetup.ExitCode -ne 0) {
|
|
throw "Could not prepare the native candidate write smoke test`n$candidateWriteSetupError"
|
|
}
|
|
$candidateWriteSetup.Dispose()
|
|
$candidateWriteTarget = $candidateWriteSetupOutput | ConvertFrom-Json
|
|
|
|
$nativeRegistrationBody = @{
|
|
examId = $candidateWriteTarget.examId
|
|
subjectIds = @($candidateWriteTarget.subjectId)
|
|
} | ConvertTo-Json -Compress
|
|
$nativeRegistrationResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/registrations" -Method Post -ContentType 'application/json' -Body $nativeRegistrationBody -WebSession $registeredSession
|
|
if ($nativeRegistrationResponse.StatusCode -ne 201 -or $nativeRegistrationResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Exam registration submission did not use the native ASP.NET Core endpoint'
|
|
}
|
|
$nativeRegistration = $nativeRegistrationResponse.Content | ConvertFrom-Json
|
|
if ($nativeRegistration.registration.status -ne 'pending' -or $nativeRegistration.registration.subjectIds.Count -ne 1) {
|
|
throw 'Native exam registration did not persist the selected subject and pending state'
|
|
}
|
|
$duplicateRegistration = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/registrations" -Method Post -ContentType 'application/json' -Body $nativeRegistrationBody -WebSession $registeredSession -SkipHttpErrorCheck
|
|
if ($duplicateRegistration.StatusCode -ne 409) {
|
|
throw 'Native exam registration did not reject a duplicate submission'
|
|
}
|
|
|
|
$legacyRegisteredSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
Invoke-RestMethod -Uri "$legacyBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $registeredLoginBody -WebSession $legacyRegisteredSession | Out-Null
|
|
foreach ($candidateWriteReadRoute in @('profile', 'registrations')) {
|
|
$legacyWriteRead = Invoke-WebRequest -Uri "$legacyBaseUrl/api/candidate/$candidateWriteReadRoute" -WebSession $legacyRegisteredSession
|
|
$nativeWriteRead = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/$candidateWriteReadRoute" -WebSession $registeredSession
|
|
Assert-JsonEquivalent -Expected $legacyWriteRead.Content -Actual $nativeWriteRead.Content -Label "Candidate write follow-up '$candidateWriteReadRoute'"
|
|
}
|
|
|
|
$nativeSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
$nativeLoginResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $loginBody -WebSession $nativeSession
|
|
if ($nativeLoginResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Authentication login request did not use the native ASP.NET Core endpoint'
|
|
}
|
|
$nativeLogin = $nativeLoginResponse.Content | ConvertFrom-Json
|
|
if ($nativeLogin.ok -ne $true -or $nativeLogin.user.username -ne 'admin') {
|
|
throw 'Native authentication could not verify the existing Node PBKDF2 account'
|
|
}
|
|
foreach ($adminReadRoute in @('context', 'dashboard', 'schools', 'admins', 'exams')) {
|
|
$legacyAdminRead = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/$adminReadRoute" -WebSession $session
|
|
$nativeAdminRead = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/$adminReadRoute" -WebSession $nativeSession
|
|
if ($nativeAdminRead.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw "Admin read route '$adminReadRoute' did not use the native ASP.NET Core endpoint"
|
|
}
|
|
Assert-JsonEquivalent -Expected $legacyAdminRead.Content -Actual $nativeAdminRead.Content -Label "Admin read route '$adminReadRoute'"
|
|
}
|
|
$superSchoolOrganization = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/school-organization" -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($superSchoolOrganization.StatusCode -ne 403 -or $superSchoolOrganization.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native school-organization route did not preserve the school-level authorization boundary'
|
|
}
|
|
$schoolLoginBody = @{ username = 'school_admin'; password = '12345678' } | ConvertTo-Json -Compress
|
|
$legacySchoolSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
$nativeSchoolSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
Invoke-RestMethod -Uri "$legacyBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $schoolLoginBody -WebSession $legacySchoolSession | Out-Null
|
|
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $schoolLoginBody -WebSession $nativeSchoolSession | Out-Null
|
|
foreach ($schoolAdminRoute in @('context', 'dashboard', 'school-organization', 'admins')) {
|
|
$legacySchoolRead = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/$schoolAdminRoute" -WebSession $legacySchoolSession
|
|
$nativeSchoolRead = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/$schoolAdminRoute" -WebSession $nativeSchoolSession
|
|
Assert-JsonEquivalent -Expected $legacySchoolRead.Content -Actual $nativeSchoolRead.Content -Label "School admin read route '$schoolAdminRoute'"
|
|
}
|
|
foreach ($superOnlyRoute in @('schools', 'exams')) {
|
|
$schoolForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/$superOnlyRoute" -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($schoolForbidden.StatusCode -ne 403 -or $schoolForbidden.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw "Native admin route '$superOnlyRoute' did not preserve the super-admin boundary"
|
|
}
|
|
}
|
|
$legacyNoticeManagement = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/notices" -WebSession $session
|
|
$nativeNoticeManagement = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/notices" -WebSession $nativeSession
|
|
if ($nativeNoticeManagement.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native notice management list did not use ASP.NET Core'
|
|
}
|
|
Assert-JsonEquivalent -Expected $legacyNoticeManagement.Content -Actual $nativeNoticeManagement.Content -Label 'Admin notice management list'
|
|
$schoolNoticeManagementForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/notices" -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($schoolNoticeManagementForbidden.StatusCode -ne 403 -or $schoolNoticeManagementForbidden.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native notice management list did not preserve the super-admin boundary'
|
|
}
|
|
|
|
$noticeManagementState = $nativeNoticeManagement.Content | ConvertFrom-Json
|
|
$managedPlan = @($noticeManagementState.publications | Where-Object { $_.sourceType -eq 'plan' })[0]
|
|
if ($null -eq $managedPlan) {
|
|
throw 'Seed data did not provide a system plan publication for notice-management smoke testing'
|
|
}
|
|
$invalidPublicationVisibility = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/publications/plan/$($managedPlan.id)" -Method Patch -ContentType 'application/json' -Body (@{ visible = 'false' } | ConvertTo-Json -Compress) -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidPublicationVisibility.StatusCode -ne 400 -or $invalidPublicationVisibility.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native system-publication update accepted a non-boolean visibility value'
|
|
}
|
|
$missingPublication = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/publications/plan/missing-publication" -Method Patch -ContentType 'application/json' -Body (@{ visible = $false } | ConvertTo-Json -Compress) -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($missingPublication.StatusCode -ne 404) {
|
|
throw 'Native system-publication update did not return 404 for a missing record'
|
|
}
|
|
$schoolPublicationForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/publications/plan/$($managedPlan.id)" -Method Patch -ContentType 'application/json' -Body (@{ visible = $false } | ConvertTo-Json -Compress) -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($schoolPublicationForbidden.StatusCode -ne 403) {
|
|
throw 'Native system-publication update did not preserve the super-admin boundary'
|
|
}
|
|
$hiddenPublicationResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/publications/plan/$($managedPlan.id)" -Method Patch -ContentType 'application/json' -Body (@{ visible = $false } | ConvertTo-Json -Compress) -WebSession $nativeSession
|
|
$hiddenPublication = ($hiddenPublicationResponse.Content | ConvertFrom-Json).publication
|
|
if ($hiddenPublicationResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $hiddenPublication.visible -ne $false -or $hiddenPublication.status -ne 'hidden') {
|
|
throw 'Native system-publication update did not hide the requested publication'
|
|
}
|
|
$hiddenAnnouncements = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/public/announcements"
|
|
if (@($hiddenAnnouncements.plans | Where-Object { $_.id -eq $managedPlan.id }).Count -ne 0) {
|
|
throw 'Native public announcements still exposed a hidden system publication'
|
|
}
|
|
$restoredPublicationResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/publications/plan/$($managedPlan.id)" -Method Patch -ContentType 'application/json' -Body (@{ visible = $true } | ConvertTo-Json -Compress) -WebSession $nativeSession
|
|
$restoredPublication = ($restoredPublicationResponse.Content | ConvertFrom-Json).publication
|
|
if ($restoredPublication.visible -ne $true -or $restoredPublication.status -ne 'visible') {
|
|
throw 'Native system-publication update did not restore the requested publication'
|
|
}
|
|
$restoredAnnouncements = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/public/announcements"
|
|
if (@($restoredAnnouncements.plans | Where-Object { $_.id -eq $managedPlan.id }).Count -ne 1) {
|
|
throw 'Native public announcements did not restore a visible system publication'
|
|
}
|
|
$classLoginBody = @{ username = 'class_admin'; password = '12345678' } | ConvertTo-Json -Compress
|
|
$legacyClassSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
$nativeClassSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
Invoke-RestMethod -Uri "$legacyBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $classLoginBody -WebSession $legacyClassSession | Out-Null
|
|
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $classLoginBody -WebSession $nativeClassSession | Out-Null
|
|
foreach ($classAdminRoute in @('context', 'dashboard')) {
|
|
$legacyClassRead = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/$classAdminRoute" -WebSession $legacyClassSession
|
|
$nativeClassRead = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/$classAdminRoute" -WebSession $nativeClassSession
|
|
Assert-JsonEquivalent -Expected $legacyClassRead.Content -Actual $nativeClassRead.Content -Label "Class admin read route '$classAdminRoute'"
|
|
}
|
|
$classAdminsForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admins" -WebSession $nativeClassSession -SkipHttpErrorCheck
|
|
if ($classAdminsForbidden.StatusCode -ne 403) {
|
|
throw 'Native admins route did not preserve the class-admin boundary'
|
|
}
|
|
|
|
$legacyAdmissions = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/admissions" -WebSession $session
|
|
$nativeAdmissions = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admissions" -WebSession $nativeSession
|
|
if ($nativeAdmissions.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native admission management did not use ASP.NET Core'
|
|
}
|
|
Assert-JsonEquivalent -Expected $legacyAdmissions.Content -Actual $nativeAdmissions.Content -Label 'Admission management workspace'
|
|
$schoolAdmissionsForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admissions" -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($schoolAdmissionsForbidden.StatusCode -ne 403) {
|
|
throw 'Native admission management did not preserve the super-admin boundary'
|
|
}
|
|
$legacyQualifications = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/indicator-qualifications" -WebSession $legacySchoolSession
|
|
$nativeQualifications = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/indicator-qualifications" -WebSession $nativeSchoolSession
|
|
if ($nativeQualifications.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native indicator-qualification workspace did not use ASP.NET Core'
|
|
}
|
|
Assert-JsonEquivalent -Expected $legacyQualifications.Content -Actual $nativeQualifications.Content -Label 'Indicator qualification workspace'
|
|
$invalidAdmissionSetting = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admissions/missing-exam/setting" -Method Put -ContentType 'application/json' -Body '{}' -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidAdmissionSetting.StatusCode -ne 404 -or $invalidAdmissionSetting.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native admission setting did not reject a missing exam'
|
|
}
|
|
$nativeAdmissionState = $nativeAdmissions.Content | ConvertFrom-Json
|
|
$admissionExam = @($nativeAdmissionState.exams | Select-Object -First 1)[0]
|
|
$admissionSchool = @($nativeAdmissionState.admissionSchools | Select-Object -First 1)[0]
|
|
if ($null -ne $admissionExam -and $null -ne $admissionSchool) {
|
|
$invalidAdmissionPlanBody = @{
|
|
examId = $admissionExam.id
|
|
schoolId = $admissionSchool.id
|
|
categories = @(
|
|
@{
|
|
code = 'invalid-specialty'
|
|
name = '无效特长类别'
|
|
quota = 1
|
|
specialtyCategory = 'sports'
|
|
specialtyType = 'vocal_music'
|
|
indicatorAllocations = @()
|
|
}
|
|
)
|
|
} | ConvertTo-Json -Depth 6 -Compress
|
|
$invalidAdmissionPlan = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admission-plans" -Method Post -ContentType 'application/json' -Body $invalidAdmissionPlanBody -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidAdmissionPlan.StatusCode -ne 400 -or $invalidAdmissionPlan.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native admission plan accepted a mismatched specialty category and type'
|
|
}
|
|
}
|
|
$unsafeAdminDelete = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admins/unsafe-delete" -Method Delete -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($unsafeAdminDelete.StatusCode -ne 405 -or $unsafeAdminDelete.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native admin management did not preserve the no-delete safety boundary'
|
|
}
|
|
|
|
$arrangementSessions = @(
|
|
@{ Level = 'super'; Legacy = $session; Native = $nativeSession },
|
|
@{ Level = 'school'; Legacy = $legacySchoolSession; Native = $nativeSchoolSession },
|
|
@{ Level = 'class'; Legacy = $legacyClassSession; Native = $nativeClassSession }
|
|
)
|
|
foreach ($arrangementSession in $arrangementSessions) {
|
|
$legacyArrangementRead = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/admission-arrangements" -WebSession $arrangementSession.Legacy
|
|
$nativeArrangementRead = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admission-arrangements" -WebSession $arrangementSession.Native
|
|
if ($nativeArrangementRead.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw "Arrangement read for $($arrangementSession.Level) admin did not use ASP.NET Core"
|
|
}
|
|
Assert-JsonEquivalent -Expected $legacyArrangementRead.Content -Actual $nativeArrangementRead.Content -Label "Arrangement read for $($arrangementSession.Level) admin"
|
|
}
|
|
$arrangementState = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/admission-arrangements" -WebSession $nativeSession
|
|
$arrangementExam = @($arrangementState.exams | Where-Object { $_.approvedCount -gt 0 -and -not $_.archivedAt })[0]
|
|
$arrangementRule = @($arrangementState.rules | Where-Object { $_.code -eq 'district_room_seat' })[0]
|
|
if ($null -eq $arrangementExam -or $null -eq $arrangementRule) {
|
|
throw 'Seed data did not provide an exam and number rule for arrangement smoke testing'
|
|
}
|
|
$arrangementBody = @{
|
|
mixingScope = 'province'
|
|
numberRuleId = $arrangementRule.id
|
|
seed = 'native-arrangement-parity'
|
|
} | ConvertTo-Json -Compress
|
|
$schoolArrangementForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams/$($arrangementExam.id)/admission-arrangement/preview" -Method Post -ContentType 'application/json' -Body $arrangementBody -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($schoolArrangementForbidden.StatusCode -ne 403 -or $schoolArrangementForbidden.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native arrangement preview did not preserve the super-admin boundary'
|
|
}
|
|
$invalidArrangementBody = @{
|
|
mixingScope = 'invalid'
|
|
numberRuleId = $arrangementRule.id
|
|
seed = 'native-arrangement-parity'
|
|
} | ConvertTo-Json -Compress
|
|
$invalidArrangement = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams/$($arrangementExam.id)/admission-arrangement/preview" -Method Post -ContentType 'application/json' -Body $invalidArrangementBody -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidArrangement.StatusCode -ne 400) {
|
|
throw 'Native arrangement preview accepted an invalid mixing scope'
|
|
}
|
|
$legacyArrangementPreview = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/exams/$($arrangementExam.id)/admission-arrangement/preview" -Method Post -ContentType 'application/json' -Body $arrangementBody -WebSession $session
|
|
$nativeArrangementPreview = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams/$($arrangementExam.id)/admission-arrangement/preview" -Method Post -ContentType 'application/json' -Body $arrangementBody -WebSession $nativeSession
|
|
if ($nativeArrangementPreview.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native arrangement preview did not use ASP.NET Core'
|
|
}
|
|
Assert-ArrangementPreviewEquivalent -Expected $legacyArrangementPreview.Content -Actual $nativeArrangementPreview.Content
|
|
|
|
$arrangementApplyResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams/$($arrangementExam.id)/admission-arrangement" -Method Post -ContentType 'application/json' -Body $arrangementBody -WebSession $nativeSession
|
|
$appliedArrangement = $arrangementApplyResponse.Content | ConvertFrom-Json
|
|
if ($arrangementApplyResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or
|
|
$appliedArrangement.plan.examId -ne $arrangementExam.id -or
|
|
$appliedArrangement.summary.candidateCount -le 0 -or
|
|
@($appliedArrangement.cards).Count -ne $appliedArrangement.summary.candidateCount) {
|
|
throw 'Native arrangement application did not persist the complete exam plan'
|
|
}
|
|
foreach ($arrangementSession in $arrangementSessions) {
|
|
$legacyArrangementAfterApply = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/admission-arrangements" -WebSession $arrangementSession.Legacy
|
|
$nativeArrangementAfterApply = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admission-arrangements" -WebSession $arrangementSession.Native
|
|
Assert-JsonEquivalent -Expected $legacyArrangementAfterApply.Content -Actual $nativeArrangementAfterApply.Content -Label "Arrangement application follow-up for $($arrangementSession.Level) admin"
|
|
}
|
|
$legacySingleAdmit = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/registrations/$($appliedArrangement.cards[0].registrationId)/admit-card" -Method Post -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($legacySingleAdmit.StatusCode -ne 410 -or $legacySingleAdmit.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native arrangement routes did not retire single-candidate admit-card generation'
|
|
}
|
|
$anonymousSingleAdmit = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/registrations/$($appliedArrangement.cards[0].registrationId)/admit-card" -Method Post -SkipHttpErrorCheck
|
|
if ($anonymousSingleAdmit.StatusCode -ne 401) {
|
|
throw 'Native retired admit-card route did not preserve administrator authentication'
|
|
}
|
|
$batchAdmitExport = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admission-exports/admit-cards?examId=$($arrangementExam.id)" -WebSession $nativeSession
|
|
if ($batchAdmitExport.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or
|
|
$batchAdmitExport.Headers['Content-Type'] -notmatch '^text/html' -or
|
|
$batchAdmitExport.Content -notmatch '衡准 · 准考证') {
|
|
throw 'Native batch admit-card HTML export is invalid'
|
|
}
|
|
$admitInfoExport = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admission-exports/info?examId=$($arrangementExam.id)" -WebSession $nativeSession
|
|
if ($admitInfoExport.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or
|
|
$admitInfoExport.Headers['Content-Type'] -notmatch 'spreadsheetml' -or
|
|
$admitInfoExport.RawContentLength -lt 1000) {
|
|
throw 'Native admit-card information workbook export is invalid'
|
|
}
|
|
$superCenterMaterialsForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admission-exports/center-materials?examId=$($arrangementExam.id)" -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($superCenterMaterialsForbidden.StatusCode -ne 403) {
|
|
throw 'Native center-material export did not preserve the school-admin boundary'
|
|
}
|
|
$centerMaterialsExport = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admission-exports/center-materials?examId=$($arrangementExam.id)" -WebSession $nativeSchoolSession
|
|
if ($centerMaterialsExport.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or
|
|
$centerMaterialsExport.Headers['Content-Type'] -notmatch 'spreadsheetml' -or
|
|
$centerMaterialsExport.RawContentLength -lt 1000) {
|
|
throw 'Native center-material workbook export is invalid'
|
|
}
|
|
|
|
$legacyEmptyResults = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/results" -WebSession $session
|
|
$nativeEmptyResults = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/results" -WebSession $nativeSession
|
|
if ($nativeEmptyResults.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native empty result workspace did not use ASP.NET Core'
|
|
}
|
|
Assert-JsonEquivalent -Expected $legacyEmptyResults.Content -Actual $nativeEmptyResults.Content -Label 'Empty result workspace'
|
|
|
|
$resultsUri = "/api/admin/results?examId=$($arrangementExam.id)"
|
|
$legacyResults = Invoke-WebRequest -Uri "$legacyBaseUrl$resultsUri" -WebSession $session
|
|
$nativeResults = Invoke-WebRequest -Uri "$nativeAuthBaseUrl$resultsUri" -WebSession $nativeSession
|
|
if ($nativeResults.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native result workspace did not use ASP.NET Core'
|
|
}
|
|
Assert-JsonEquivalent -Expected $legacyResults.Content -Actual $nativeResults.Content -Label 'Super-admin result workspace'
|
|
$nativeResultsState = $nativeResults.Content | ConvertFrom-Json
|
|
$resultRegistration = @($nativeResultsState.registrations | Where-Object { @($_.subjects).Count -gt 0 } | Select-Object -First 1)[0]
|
|
if ($null -eq $resultRegistration) {
|
|
throw 'Seed data did not provide an approved registration for native result writes'
|
|
}
|
|
$resultSubject = @($resultRegistration.subjects)[0]
|
|
$schoolResultWrite = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/results" -Method Post -ContentType 'application/json' -Body '{}' -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($schoolResultWrite.StatusCode -ne 403 -or $schoolResultWrite.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native result writes did not preserve the super-admin boundary'
|
|
}
|
|
$invalidResultBody = @{
|
|
registrationId = $resultRegistration.id
|
|
subjectId = $resultSubject.id
|
|
score = [double] $resultSubject.fullScore + 1
|
|
published = $false
|
|
} | ConvertTo-Json -Compress
|
|
$invalidResult = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/results" -Method Post -ContentType 'application/json' -Body $invalidResultBody -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidResult.StatusCode -ne 400) {
|
|
throw 'Native result write accepted a score above the subject full score'
|
|
}
|
|
$invalidResultImportBody = @{
|
|
rows = @(
|
|
@{
|
|
candidateNumber = ''
|
|
examCode = ''
|
|
subjectName = ''
|
|
score = 0
|
|
published = $false
|
|
}
|
|
)
|
|
} | ConvertTo-Json -Depth 4 -Compress
|
|
$invalidResultImport = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/results/import" -Method Post -ContentType 'application/json' -Body $invalidResultImportBody -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidResultImport.StatusCode -ne 400 -or $invalidResultImport.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native result import accepted an invalid preview row'
|
|
}
|
|
$score = [Math]::Min([double] $resultSubject.fullScore, 88.5)
|
|
$bulkDraftBody = @{
|
|
examId = $arrangementExam.id
|
|
subjectId = $resultSubject.id
|
|
published = $false
|
|
rows = @(@{ registrationId = $resultRegistration.id; score = $score })
|
|
} | ConvertTo-Json -Depth 5 -Compress
|
|
$bulkDraftResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/results/bulk" -Method Post -ContentType 'application/json' -Body $bulkDraftBody -WebSession $nativeSession
|
|
$bulkDraft = $bulkDraftResponse.Content | ConvertFrom-Json
|
|
if ($bulkDraftResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $bulkDraft.count -ne 1 -or $bulkDraft.published -ne $false) {
|
|
throw 'Native result bulk draft did not save the requested score'
|
|
}
|
|
$bulkPublishBody = @{
|
|
examId = $arrangementExam.id
|
|
subjectId = $resultSubject.id
|
|
published = $true
|
|
rows = @(@{ registrationId = $resultRegistration.id; score = $score })
|
|
} | ConvertTo-Json -Depth 5 -Compress
|
|
$bulkPublish = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/results/bulk" -Method Post -ContentType 'application/json' -Body $bulkPublishBody -WebSession $nativeSession
|
|
if ($bulkPublish.count -ne 1 -or $bulkPublish.published -ne $true -or $bulkPublish.summary.publish -ne 1) {
|
|
throw 'Native result bulk publication did not publish the requested score'
|
|
}
|
|
$invalidFeatureBody = @{
|
|
examId = $arrangementExam.id
|
|
rows = @(@{ registrationId = $resultRegistration.id; featureScore = 1001 })
|
|
} | ConvertTo-Json -Depth 5 -Compress
|
|
$invalidFeature = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/feature-scores/bulk" -Method Post -ContentType 'application/json' -Body $invalidFeatureBody -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidFeature.StatusCode -ne 400) {
|
|
throw 'Native feature-score bulk write accepted a score above 1000'
|
|
}
|
|
$featureBody = @{ featureScore = 19.25 } | ConvertTo-Json -Compress
|
|
$featureResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/registrations/$($resultRegistration.id)/feature-score" -Method Patch -ContentType 'application/json' -Body $featureBody -WebSession $nativeSession
|
|
$featureRegistration = ($featureResponse.Content | ConvertFrom-Json).registration
|
|
if ($featureResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $featureRegistration.featureScore -ne 19.25) {
|
|
throw 'Native feature-score write did not persist the rounded score'
|
|
}
|
|
$invalidAppeal = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/score-appeals/missing-result" -Method Patch -ContentType 'application/json' -Body '{"status":"invalid"}' -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidAppeal.StatusCode -ne 400 -or $invalidAppeal.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native score-appeal review accepted an invalid status'
|
|
}
|
|
if ($null -ne $appealResultId) {
|
|
$appealReviewIterations = 0
|
|
do {
|
|
$appealReviewIterations += 1
|
|
if ($appealReviewIterations -gt 10) {
|
|
throw 'Native score-appeal workflow did not reach a terminal state'
|
|
}
|
|
$appealReviewBody = @{
|
|
status = 'approved'
|
|
reviewedScore = $appealOriginalScore
|
|
reviewNote = "原生成绩复议第 $appealReviewIterations 步"
|
|
} | ConvertTo-Json -Compress
|
|
$appealReviewResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/score-appeals/$appealResultId" -Method Patch -ContentType 'application/json' -Body $appealReviewBody -WebSession $nativeSession
|
|
$appealReview = $appealReviewResponse.Content | ConvertFrom-Json
|
|
if ($appealReviewResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native score-appeal review did not use ASP.NET Core'
|
|
}
|
|
} while ($appealReview.workflow.status -eq 'pending')
|
|
if ($appealReview.workflow.status -ne 'approved' -or $appealReview.result.score -ne $appealOriginalScore) {
|
|
throw 'Native score-appeal review did not complete the workflow and preserve the reviewed score'
|
|
}
|
|
if ($appealExamId) {
|
|
$appealResultsUri = "/api/admin/results?examId=$appealExamId"
|
|
$legacyAppealResults = Invoke-WebRequest -Uri "$legacyBaseUrl$appealResultsUri" -WebSession $session
|
|
$nativeAppealResults = Invoke-WebRequest -Uri "$nativeAuthBaseUrl$appealResultsUri" -WebSession $nativeSession
|
|
Assert-JsonEquivalent -Expected $legacyAppealResults.Content -Actual $nativeAppealResults.Content -Label 'Score-appeal review follow-up'
|
|
}
|
|
}
|
|
$resultExport = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/excel/results?examId=$($arrangementExam.id)" -WebSession $nativeSession
|
|
if ($resultExport.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or
|
|
$resultExport.Headers['Content-Type'] -notmatch 'spreadsheetml' -or
|
|
$resultExport.RawContentLength -lt 1000) {
|
|
throw 'Native result workbook export is invalid'
|
|
}
|
|
foreach ($excelResource in @('classes', 'class_admins', 'account_quotas', 'candidates', 'payments', 'centers')) {
|
|
$excelExport = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/excel/$excelResource" -WebSession $nativeSession
|
|
if ($excelExport.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or
|
|
$excelExport.Headers['Content-Type'] -notmatch 'spreadsheetml' -or
|
|
$excelExport.RawContentLength -lt 1000) {
|
|
throw "Native '$excelResource' workbook export is invalid"
|
|
}
|
|
}
|
|
$accountResultTemplate = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/excel/account_results?template=1" -WebSession $nativeSession
|
|
if ($accountResultTemplate.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or
|
|
$accountResultTemplate.Headers['Content-Type'] -notmatch 'spreadsheetml' -or
|
|
$accountResultTemplate.RawContentLength -lt 1000) {
|
|
throw "Native 'account_results' workbook template is invalid"
|
|
}
|
|
foreach ($ledgerKind in @('preferences', 'placements')) {
|
|
$ledgerExport = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admissions/$ledgerKind/export" -WebSession $nativeSession
|
|
if ($ledgerExport.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or
|
|
$ledgerExport.Headers['Content-Type'] -notmatch 'spreadsheetml' -or
|
|
$ledgerExport.RawContentLength -lt 1000) {
|
|
throw "Native admission '$ledgerKind' ledger workbook export is invalid"
|
|
}
|
|
}
|
|
$admissionSchoolSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
$admissionSchoolLoginBody = @{ username = 'admission_1_admin'; password = '12345678' } | ConvertTo-Json -Compress
|
|
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $admissionSchoolLoginBody -WebSession $admissionSchoolSession | Out-Null
|
|
foreach ($admissionSchoolRoute in @('notice-template', 'reporting', 'placements')) {
|
|
$admissionSchoolResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admission/$admissionSchoolRoute" -WebSession $admissionSchoolSession
|
|
if ($admissionSchoolResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw "Native admission-school route '$admissionSchoolRoute' did not use ASP.NET Core"
|
|
}
|
|
}
|
|
$legacyResultsAfterWrites = Invoke-WebRequest -Uri "$legacyBaseUrl$resultsUri" -WebSession $session
|
|
$nativeResultsAfterWrites = Invoke-WebRequest -Uri "$nativeAuthBaseUrl$resultsUri" -WebSession $nativeSession
|
|
Assert-JsonEquivalent -Expected $legacyResultsAfterWrites.Content -Actual $nativeResultsAfterWrites.Content -Label 'Result writes follow-up'
|
|
|
|
$operationalSessions = @(
|
|
@{ Level = 'super'; Legacy = $session; Native = $nativeSession },
|
|
@{ Level = 'school'; Legacy = $legacySchoolSession; Native = $nativeSchoolSession },
|
|
@{ Level = 'class'; Legacy = $legacyClassSession; Native = $nativeClassSession }
|
|
)
|
|
foreach ($operationalSession in $operationalSessions) {
|
|
foreach ($operationalRoute in @('candidates', 'registrations', 'payments')) {
|
|
$legacyOperationalRead = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/$operationalRoute" -WebSession $operationalSession.Legacy
|
|
$nativeOperationalRead = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/$operationalRoute" -WebSession $operationalSession.Native
|
|
if ($nativeOperationalRead.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw "Admin operational route '$operationalRoute' for $($operationalSession.Level) admin did not use ASP.NET Core"
|
|
}
|
|
Assert-JsonEquivalent -Expected $legacyOperationalRead.Content -Actual $nativeOperationalRead.Content -Label "Admin operational route '$operationalRoute' for $($operationalSession.Level) admin"
|
|
}
|
|
}
|
|
|
|
foreach ($workflowSession in $operationalSessions) {
|
|
$legacyWorkflowRead = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/workflow-instances" -WebSession $workflowSession.Legacy
|
|
$nativeWorkflowRead = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/workflow-instances" -WebSession $workflowSession.Native
|
|
if ($nativeWorkflowRead.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw "Workflow inbox for $($workflowSession.Level) admin did not use ASP.NET Core"
|
|
}
|
|
Assert-JsonEquivalent -Expected $legacyWorkflowRead.Content -Actual $nativeWorkflowRead.Content -Label "Workflow inbox for $($workflowSession.Level) admin"
|
|
}
|
|
$workflowState = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/workflow-instances" -WebSession $nativeSession
|
|
$transferInstance = @($workflowState.instances | Where-Object { $_.status -eq 'pending' -and $_.assigneeId } | Select-Object -First 1)[0]
|
|
if ($null -eq $transferInstance) {
|
|
throw 'Seed data did not provide a pending workflow for transfer testing'
|
|
}
|
|
$invalidTransferBody = @{ assigneeId = 'missing-admin'; note = '无效转交测试' } | ConvertTo-Json -Compress
|
|
$invalidTransfer = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/workflow-instances/$($transferInstance.id)/transfer" -Method Patch -ContentType 'application/json' -Body $invalidTransferBody -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidTransfer.StatusCode -ne 400 -or $invalidTransfer.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native workflow transfer accepted an invalid target administrator'
|
|
}
|
|
$transferBody = @{ assigneeId = $transferInstance.assigneeId; note = '原生同级转交验证' } | ConvertTo-Json -Compress
|
|
$transferResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/workflow-instances/$($transferInstance.id)/transfer" -Method Patch -ContentType 'application/json' -Body $transferBody -WebSession $nativeSession
|
|
$transferredWorkflow = ($transferResponse.Content | ConvertFrom-Json).workflow
|
|
if ($transferResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $transferredWorkflow.assigneeId -ne $transferInstance.assigneeId -or @($transferredWorkflow.actions | Select-Object -Last 1)[0].action -ne 'transfer') {
|
|
throw 'Native workflow transfer did not persist its action and assignee'
|
|
}
|
|
$legacyWorkflowsAfterTransfer = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/workflow-instances" -WebSession $session
|
|
$nativeWorkflowsAfterTransfer = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/workflow-instances" -WebSession $nativeSession
|
|
Assert-JsonEquivalent -Expected $legacyWorkflowsAfterTransfer.Content -Actual $nativeWorkflowsAfterTransfer.Content -Label 'Workflow transfer follow-up'
|
|
|
|
$archiveBody = @{
|
|
scopeType = 'class'
|
|
scopeValue = $profileUpdate.profile.classId
|
|
archived = $true
|
|
} | ConvertTo-Json -Compress
|
|
$superArchiveForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-accounts/archive" -Method Post -ContentType 'application/json' -Body $archiveBody -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($superArchiveForbidden.StatusCode -ne 403 -or $superArchiveForbidden.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native candidate archive did not preserve the school-admin boundary'
|
|
}
|
|
$classArchiveForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-accounts/archive" -Method Post -ContentType 'application/json' -Body $archiveBody -WebSession $nativeClassSession -SkipHttpErrorCheck
|
|
if ($classArchiveForbidden.StatusCode -ne 403) {
|
|
throw 'Native candidate archive accepted a class administrator'
|
|
}
|
|
$archiveResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-accounts/archive" -Method Post -ContentType 'application/json' -Body $archiveBody -WebSession $nativeSchoolSession
|
|
$archiveResult = $archiveResponse.Content | ConvertFrom-Json
|
|
if ($archiveResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $archiveResult.archived -ne $true -or $archiveResult.count -lt 1) {
|
|
throw 'Native candidate archive did not freeze the selected class accounts'
|
|
}
|
|
$archivedState = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/candidates" -WebSession $nativeSession
|
|
$archivedCandidate = @($archivedState.candidates | Where-Object id -eq $profileUpdate.profile.id)[0]
|
|
if ($null -eq $archivedCandidate -or $archivedCandidate.accountArchived -ne $true) {
|
|
throw 'Native candidate archive was not reflected by the candidate read model'
|
|
}
|
|
|
|
$restoreBody = @{
|
|
scopeType = 'class'
|
|
scopeValue = $profileUpdate.profile.classId
|
|
archived = $false
|
|
} | ConvertTo-Json -Compress
|
|
$restoreResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-accounts/archive" -Method Post -ContentType 'application/json' -Body $restoreBody -WebSession $nativeSchoolSession
|
|
$restoreResult = $restoreResponse.Content | ConvertFrom-Json
|
|
if ($restoreResult.archived -ne $false -or $restoreResult.count -ne $archiveResult.count) {
|
|
throw 'Native candidate archive could not restore the same class accounts'
|
|
}
|
|
|
|
$preResetSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $registeredLoginBody -WebSession $preResetSession | Out-Null
|
|
$schoolResetForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidates/$($profileUpdate.profile.id)/reset-password" -Method Post -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($schoolResetForbidden.StatusCode -ne 403 -or $schoolResetForbidden.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native candidate password reset did not preserve the super-admin boundary'
|
|
}
|
|
$resetCandidateResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidates/$($profileUpdate.profile.id)/reset-password" -Method Post -WebSession $nativeSession
|
|
$resetCandidate = $resetCandidateResponse.Content | ConvertFrom-Json
|
|
if ($resetCandidateResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $resetCandidate.candidateNumber -ne $registration.registrationNumber -or $resetCandidate.temporaryPassword -notmatch '^Reset-[A-Za-z0-9_-]+$') {
|
|
throw 'Native candidate password reset did not return compatible temporary credentials'
|
|
}
|
|
$invalidatedCandidateSession = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/me" -WebSession $preResetSession
|
|
if ($null -ne $invalidatedCandidateSession.user) {
|
|
throw 'Native candidate password reset did not invalidate existing sessions'
|
|
}
|
|
$resetCandidateLoginBody = @{ username = $registration.registrationNumber; password = $resetCandidate.temporaryPassword } | ConvertTo-Json -Compress
|
|
$resetCandidateSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
$resetCandidateLogin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $resetCandidateLoginBody -WebSession $resetCandidateSession
|
|
if ($resetCandidateLogin.user.mustChangePassword -ne $true) {
|
|
throw 'Native candidate temporary password did not require a password change'
|
|
}
|
|
|
|
$invalidProfileReview = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidates/$($profileUpdate.profile.id)" -Method Patch -ContentType 'application/json' -Body '{"status":"invalid"}' -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidProfileReview.StatusCode -ne 400 -or $invalidProfileReview.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native candidate profile review accepted an invalid status'
|
|
}
|
|
$candidateManagementState = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/candidates" -WebSession $nativeSession
|
|
$managedCandidate = @($candidateManagementState.candidates | Where-Object id -eq $profileUpdate.profile.id)[0]
|
|
if ($null -eq $managedCandidate -or $managedCandidate.workflow.status -ne 'pending') {
|
|
throw 'Candidate profile smoke data did not retain a pending review workflow'
|
|
}
|
|
$reviewIterations = 0
|
|
do {
|
|
$reviewIterations += 1
|
|
if ($reviewIterations -gt 10) {
|
|
throw 'Native candidate profile workflow did not reach a terminal state'
|
|
}
|
|
$profileReviewBody = @{ status = 'approved'; reviewNote = "原生资料审核第 $reviewIterations 步" } | ConvertTo-Json -Compress
|
|
$profileReviewResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidates/$($profileUpdate.profile.id)" -Method Patch -ContentType 'application/json' -Body $profileReviewBody -WebSession $nativeSession
|
|
if ($profileReviewResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Candidate profile review did not use ASP.NET Core'
|
|
}
|
|
$profileReview = $profileReviewResponse.Content | ConvertFrom-Json
|
|
} while ($profileReview.workflow.status -eq 'pending')
|
|
if ($profileReview.profile.status -ne 'approved' -or $profileReview.workflow.status -ne 'approved' -or $profileReview.workflow.actions.Count -lt 2) {
|
|
throw 'Native candidate profile review did not complete its configured workflow'
|
|
}
|
|
$legacyCandidatesAfterManagement = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/candidates" -WebSession $session
|
|
$nativeCandidatesAfterManagement = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidates" -WebSession $nativeSession
|
|
Assert-JsonEquivalent -Expected $legacyCandidatesAfterManagement.Content -Actual $nativeCandidatesAfterManagement.Content -Label 'Candidate management follow-up'
|
|
|
|
$prematurePayment = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/payments/$($nativeRegistration.registration.id)" -Method Patch -ContentType 'application/json' -Body '{}' -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($prematurePayment.StatusCode -ne 409 -or $prematurePayment.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native payment update did not require an approved registration'
|
|
}
|
|
$invalidRegistrationReview = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/registrations/$($nativeRegistration.registration.id)" -Method Patch -ContentType 'application/json' -Body '{"status":"invalid"}' -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidRegistrationReview.StatusCode -ne 400 -or $invalidRegistrationReview.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native registration review accepted an invalid status'
|
|
}
|
|
$registrationReviewIterations = 0
|
|
do {
|
|
$registrationReviewIterations += 1
|
|
if ($registrationReviewIterations -gt 10) {
|
|
throw 'Native registration workflow did not reach a terminal state'
|
|
}
|
|
$registrationReviewBody = @{ status = 'approved'; reviewNote = "原生报名审核第 $registrationReviewIterations 步" } | ConvertTo-Json -Compress
|
|
$registrationReviewResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/registrations/$($nativeRegistration.registration.id)" -Method Patch -ContentType 'application/json' -Body $registrationReviewBody -WebSession $nativeSession
|
|
if ($registrationReviewResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Registration review did not use ASP.NET Core'
|
|
}
|
|
$registrationReview = $registrationReviewResponse.Content | ConvertFrom-Json
|
|
} while ($registrationReview.workflow.status -eq 'pending')
|
|
if ($registrationReview.registration.status -ne 'approved' -or $registrationReview.workflow.status -ne 'approved' -or $registrationReview.registration.registrationNumber -ne $registration.registrationNumber) {
|
|
throw 'Native registration review did not complete the configured workflow or bind the candidate number'
|
|
}
|
|
$duplicateRegistrationReview = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/registrations/$($nativeRegistration.registration.id)" -Method Patch -ContentType 'application/json' -Body $registrationReviewBody -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($duplicateRegistrationReview.StatusCode -ne 409) {
|
|
throw 'Native registration review did not protect a completed workflow'
|
|
}
|
|
$legacyRegistrationsAfterReview = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/registrations" -WebSession $session
|
|
$nativeRegistrationsAfterReview = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/registrations" -WebSession $nativeSession
|
|
Assert-JsonEquivalent -Expected $legacyRegistrationsAfterReview.Content -Actual $nativeRegistrationsAfterReview.Content -Label 'Registration review follow-up'
|
|
|
|
$invalidPayment = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/payments/$($nativeRegistration.registration.id)" -Method Patch -ContentType 'application/json' -Body '{"status":"settled"}' -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidPayment.StatusCode -ne 400 -or $invalidPayment.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native payment update accepted an invalid status'
|
|
}
|
|
$paidResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/payments/$($nativeRegistration.registration.id)" -Method Patch -ContentType 'application/json' -Body '{}' -WebSession $nativeSession
|
|
$paidResult = ($paidResponse.Content | ConvertFrom-Json).payment
|
|
if ($paidResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $paidResult.status -ne 'paid' -or -not $paidResult.paidAt -or $paidResult.paidByName -ne $nativeLogin.user.displayName) {
|
|
throw 'Native payment update did not record the payment actor and timestamp'
|
|
}
|
|
$duplicatePayment = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/payments/$($nativeRegistration.registration.id)" -Method Patch -ContentType 'application/json' -Body '{}' -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($duplicatePayment.StatusCode -ne 409) {
|
|
throw 'Native payment update did not reject an unchanged status'
|
|
}
|
|
$legacyPaymentsAfterConfirmation = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/payments" -WebSession $session
|
|
$nativePaymentsAfterConfirmation = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/payments" -WebSession $nativeSession
|
|
Assert-JsonEquivalent -Expected $legacyPaymentsAfterConfirmation.Content -Actual $nativePaymentsAfterConfirmation.Content -Label 'Payment confirmation follow-up'
|
|
$unpaidBody = @{ status = 'unpaid' } | ConvertTo-Json -Compress
|
|
$unpaidResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/payments/$($nativeRegistration.registration.id)" -Method Patch -ContentType 'application/json' -Body $unpaidBody -WebSession $nativeSession
|
|
$unpaidResult = ($unpaidResponse.Content | ConvertFrom-Json).payment
|
|
if ($unpaidResult.status -ne 'unpaid' -or $null -ne $unpaidResult.paidAt -or $null -ne $unpaidResult.paidBy -or $unpaidResult.paidByName -ne '') {
|
|
throw 'Native payment update did not clear payment metadata when reverting to unpaid'
|
|
}
|
|
$legacyPaymentsAfterReversal = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/payments" -WebSession $session
|
|
$nativePaymentsAfterReversal = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/payments" -WebSession $nativeSession
|
|
Assert-JsonEquivalent -Expected $legacyPaymentsAfterReversal.Content -Actual $nativePaymentsAfterReversal.Content -Label 'Payment reversal follow-up'
|
|
|
|
$schoolSuperviseForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/workflow-instances/$($registrationReview.workflow.id)/supervise" -Method Patch -ContentType 'application/json' -Body '{}' -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($schoolSuperviseForbidden.StatusCode -ne 403 -or $schoolSuperviseForbidden.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native workflow supervision did not preserve the super-admin boundary'
|
|
}
|
|
$superviseBody = @{ currentStep = 1; note = '原生监督重新打开报名流程' } | ConvertTo-Json -Compress
|
|
$superviseResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/workflow-instances/$($registrationReview.workflow.id)/supervise" -Method Patch -ContentType 'application/json' -Body $superviseBody -WebSession $nativeSession
|
|
$supervisedWorkflow = ($superviseResponse.Content | ConvertFrom-Json).workflow
|
|
if ($superviseResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $supervisedWorkflow.status -ne 'pending' -or $supervisedWorkflow.currentStep -ne 1 -or @($supervisedWorkflow.actions | Select-Object -Last 1)[0].action -ne 'return') {
|
|
throw 'Native workflow supervision did not reopen the completed registration at step 1'
|
|
}
|
|
$registrationStateAfterSupervision = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/registrations" -WebSession $nativeSession
|
|
$supervisedRegistration = @($registrationStateAfterSupervision.registrations | Where-Object id -eq $nativeRegistration.registration.id)[0]
|
|
if ($null -eq $supervisedRegistration -or $supervisedRegistration.status -ne 'pending' -or $supervisedRegistration.reviewedAt) {
|
|
throw 'Native workflow supervision did not reset the registration business state'
|
|
}
|
|
foreach ($workflowSession in $operationalSessions) {
|
|
$legacyWorkflowAfterSupervision = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/workflow-instances" -WebSession $workflowSession.Legacy
|
|
$nativeWorkflowAfterSupervision = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/workflow-instances" -WebSession $workflowSession.Native
|
|
Assert-JsonEquivalent -Expected $legacyWorkflowAfterSupervision.Content -Actual $nativeWorkflowAfterSupervision.Content -Label "Workflow supervision follow-up for $($workflowSession.Level) admin"
|
|
}
|
|
|
|
$legacyCenters = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/centers" -WebSession $session
|
|
$nativeCenters = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/centers" -WebSession $nativeSession
|
|
if ($nativeCenters.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native center management list did not use ASP.NET Core'
|
|
}
|
|
Assert-JsonEquivalent -Expected $legacyCenters.Content -Actual $nativeCenters.Content -Label 'Super-admin center management'
|
|
$legacySchoolCenters = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/centers" -WebSession $legacySchoolSession
|
|
$nativeSchoolCenters = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/centers" -WebSession $nativeSchoolSession
|
|
Assert-JsonEquivalent -Expected $legacySchoolCenters.Content -Actual $nativeSchoolCenters.Content -Label 'School-admin center management'
|
|
$classCentersForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/centers" -WebSession $nativeClassSession -SkipHttpErrorCheck
|
|
if ($classCentersForbidden.StatusCode -ne 403 -or $classCentersForbidden.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native center management did not preserve the class-admin boundary'
|
|
}
|
|
|
|
$schoolCenterState = $nativeSchoolCenters.Content | ConvertFrom-Json
|
|
$templateCenter = @($schoolCenterState.centers)[0]
|
|
if ($null -eq $templateCenter) {
|
|
throw 'Seed data did not provide a school-scoped center for center-management smoke testing'
|
|
}
|
|
$invalidCenterBody = @{
|
|
code = 'NATIVE_INVALID_CENTER'
|
|
name = '无考场测试考点'
|
|
provinceCode = $templateCenter.provinceCode
|
|
cityCode = $templateCenter.cityCode
|
|
districtCode = $templateCenter.districtCode
|
|
address = '迁移测试路 10 号'
|
|
rooms = @()
|
|
} | ConvertTo-Json -Depth 5 -Compress
|
|
$invalidCenter = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/centers" -Method Post -ContentType 'application/json' -Body $invalidCenterBody -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($invalidCenter.StatusCode -ne 400) {
|
|
throw 'Native center submission accepted a center without structured rooms'
|
|
}
|
|
$centerCreateBody = @{
|
|
code = 'NATIVE_CENTER'
|
|
name = '原生迁移考点'
|
|
provinceCode = $templateCenter.provinceCode
|
|
cityCode = $templateCenter.cityCode
|
|
districtCode = $templateCenter.districtCode
|
|
address = '迁移测试路 11 号'
|
|
contact = '迁移联系人'
|
|
managerName = '考务负责人'
|
|
managerPhone = '13800001111'
|
|
emergencyPhone = '13800002222'
|
|
gateOpenTime = '07:00'
|
|
transport = '地铁迁移测试站'
|
|
status = 'active'
|
|
notes = 'ASP.NET Core 原生考点'
|
|
rooms = @(@{
|
|
code = 'NATIVE_ROOM_01'
|
|
name = '迁移第一考场'
|
|
building = '迁移楼'
|
|
floor = '1层'
|
|
capacity = 32
|
|
seatPlan = '4x8'
|
|
roomType = 'standard'
|
|
status = 'active'
|
|
notes = '原生创建'
|
|
})
|
|
} | ConvertTo-Json -Depth 6 -Compress
|
|
$centerCreateResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/centers" -Method Post -ContentType 'application/json' -Body $centerCreateBody -WebSession $nativeSchoolSession
|
|
$createdCenterChange = ($centerCreateResponse.Content | ConvertFrom-Json).changeRequest
|
|
if ($centerCreateResponse.StatusCode -ne 202 -or $centerCreateResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $createdCenterChange.status -ne 'pending' -or $createdCenterChange.workflow.assignee.adminLevel -ne 'super') {
|
|
throw 'Native center submission did not create the expected super-admin workflow'
|
|
}
|
|
$duplicateCenterSubmission = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/centers" -Method Post -ContentType 'application/json' -Body $centerCreateBody -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($duplicateCenterSubmission.StatusCode -ne 409) {
|
|
throw 'Native center submission did not reserve pending center codes'
|
|
}
|
|
$centerApprovalBody = @{ status = 'approved'; reviewNote = '原生考点终审通过' } | ConvertTo-Json -Compress
|
|
$centerApprovalResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/center-change-requests/$($createdCenterChange.id)" -Method Patch -ContentType 'application/json' -Body $centerApprovalBody -WebSession $nativeSession
|
|
if ($centerApprovalResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or ($centerApprovalResponse.Content | ConvertFrom-Json).changeRequest.status -ne 'approved') {
|
|
throw 'Native center approval did not complete the workflow'
|
|
}
|
|
$centersAfterCreate = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/centers" -WebSession $nativeSchoolSession
|
|
$createdCenter = @($centersAfterCreate.centers | Where-Object { $_.code -eq 'NATIVE_CENTER' })[0]
|
|
if ($null -eq $createdCenter -or $createdCenter.totalCapacity -ne 32 -or $createdCenter.rooms.Count -ne 1) {
|
|
throw 'Native center approval did not materialize the center and room atomically'
|
|
}
|
|
|
|
$centerUpdateBody = @{
|
|
code = $createdCenter.code
|
|
name = '原生迁移考点(更新)'
|
|
provinceCode = $createdCenter.provinceCode
|
|
cityCode = $createdCenter.cityCode
|
|
districtCode = $createdCenter.districtCode
|
|
address = '迁移测试路 12 号'
|
|
contact = $createdCenter.contact
|
|
managerName = $createdCenter.managerName
|
|
managerPhone = $createdCenter.managerPhone
|
|
emergencyPhone = $createdCenter.emergencyPhone
|
|
gateOpenTime = $createdCenter.gateOpenTime
|
|
transport = $createdCenter.transport
|
|
status = 'active'
|
|
notes = 'ASP.NET Core 原生考点已更新'
|
|
rooms = @($createdCenter.rooms | ForEach-Object {
|
|
@{
|
|
id = $_.id
|
|
code = $_.code
|
|
name = '迁移第一考场(更新)'
|
|
building = $_.building
|
|
floor = $_.floor
|
|
capacity = 36
|
|
seatPlan = $_.seatPlan
|
|
roomType = $_.roomType
|
|
status = $_.status
|
|
notes = '原生更新'
|
|
}
|
|
})
|
|
} | ConvertTo-Json -Depth 6 -Compress
|
|
$centerUpdateResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/centers/$($createdCenter.id)" -Method Patch -ContentType 'application/json' -Body $centerUpdateBody -WebSession $nativeSchoolSession
|
|
$updatedCenterChange = ($centerUpdateResponse.Content | ConvertFrom-Json).changeRequest
|
|
if ($centerUpdateResponse.StatusCode -ne 202 -or $updatedCenterChange.requestType -ne 'update') {
|
|
throw 'Native center update did not create a change workflow'
|
|
}
|
|
$duplicateCenterUpdate = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/centers/$($createdCenter.id)" -Method Patch -ContentType 'application/json' -Body $centerUpdateBody -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($duplicateCenterUpdate.StatusCode -ne 409) {
|
|
throw 'Native center update allowed a second pending change for the same center'
|
|
}
|
|
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/center-change-requests/$($updatedCenterChange.id)" -Method Patch -ContentType 'application/json' -Body $centerApprovalBody -WebSession $nativeSession | Out-Null
|
|
$centersAfterUpdate = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/centers" -WebSession $nativeSchoolSession
|
|
$updatedCenter = @($centersAfterUpdate.centers | Where-Object { $_.id -eq $createdCenter.id })[0]
|
|
if ($updatedCenter.name -ne '原生迁移考点(更新)' -or $updatedCenter.address -ne '迁移测试路 12 号' -or $updatedCenter.totalCapacity -ne 36 -or $updatedCenter.pendingChange -ne $false) {
|
|
throw 'Native center update approval did not replace the formal center archive'
|
|
}
|
|
|
|
foreach ($configurationRoute in @('number-rules', 'workflows')) {
|
|
$legacyConfiguration = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/$configurationRoute" -WebSession $session
|
|
$nativeConfiguration = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/$configurationRoute" -WebSession $nativeSession
|
|
if ($nativeConfiguration.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw "Native admin configuration route '$configurationRoute' did not use ASP.NET Core"
|
|
}
|
|
Assert-JsonEquivalent -Expected $legacyConfiguration.Content -Actual $nativeConfiguration.Content -Label "Admin configuration '$configurationRoute'"
|
|
}
|
|
foreach ($configurationRoute in @('number-rules', 'workflows')) {
|
|
$schoolConfigurationForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/$configurationRoute" -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($schoolConfigurationForbidden.StatusCode -ne 403) {
|
|
throw "Admin configuration route '$configurationRoute' did not preserve the super-admin boundary"
|
|
}
|
|
}
|
|
|
|
$numberRuleState = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/number-rules" -WebSession $nativeSession
|
|
$activeNumberRule = $numberRuleState.activeRule
|
|
$numberRuleBody = @{
|
|
id = $activeNumberRule.id
|
|
name = '原生年度学校流水号'
|
|
separator = $activeNumberRule.separator
|
|
segments = @($activeNumberRule.segments | ForEach-Object {
|
|
@{ type = $_.type; value = $_.value; width = $_.width }
|
|
})
|
|
} | ConvertTo-Json -Depth 5 -Compress
|
|
$savedNumberRule = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/number-rules" -Method Post -ContentType 'application/json' -Body $numberRuleBody -WebSession $nativeSession
|
|
if ($savedNumberRule.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or ($savedNumberRule.Content | ConvertFrom-Json).rule.name -ne '原生年度学校流水号') {
|
|
throw 'Native number-rule update did not persist the requested definition'
|
|
}
|
|
|
|
$workflowState = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/workflows" -WebSession $nativeSession
|
|
$accountWorkflow = @($workflowState.workflows | Where-Object { $_.businessType -eq 'candidate_account_batch' })[0]
|
|
$invalidWorkflowBody = @{ name = '无效流程'; steps = @(@{ name = '学校终审'; adminLevel = 'school' }) } | ConvertTo-Json -Depth 4 -Compress
|
|
$invalidWorkflow = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/workflows/candidate_account_batch" -Method Put -ContentType 'application/json' -Body $invalidWorkflowBody -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidWorkflow.StatusCode -ne 400) {
|
|
throw 'Native workflow update allowed a non-super final account-batch step'
|
|
}
|
|
$workflowBody = @{
|
|
name = '原生批量报名号审批'
|
|
steps = @($accountWorkflow.steps | ForEach-Object {
|
|
@{ name = $_.name; adminLevel = $_.adminLevel }
|
|
})
|
|
} | ConvertTo-Json -Depth 5 -Compress
|
|
$savedWorkflow = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/workflows/candidate_account_batch" -Method Put -ContentType 'application/json' -Body $workflowBody -WebSession $nativeSession
|
|
if ($savedWorkflow.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or ($savedWorkflow.Content | ConvertFrom-Json).workflow.name -ne '原生批量报名号审批') {
|
|
throw 'Native workflow update did not persist the account-batch definition'
|
|
}
|
|
|
|
$legacySuperBatches = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/candidate-account-batches" -WebSession $session
|
|
$nativeSuperBatches = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-account-batches" -WebSession $nativeSession
|
|
if ($nativeSuperBatches.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native candidate-account batch read did not use ASP.NET Core'
|
|
}
|
|
Assert-JsonEquivalent -Expected $legacySuperBatches.Content -Actual $nativeSuperBatches.Content -Label 'Super-admin candidate-account batches'
|
|
$legacySchoolBatches = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/candidate-account-batches" -WebSession $legacySchoolSession
|
|
$nativeSchoolBatches = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-account-batches" -WebSession $nativeSchoolSession
|
|
Assert-JsonEquivalent -Expected $legacySchoolBatches.Content -Actual $nativeSchoolBatches.Content -Label 'School-admin candidate-account batches'
|
|
$classBatchesForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-account-batches" -WebSession $nativeClassSession -SkipHttpErrorCheck
|
|
if ($classBatchesForbidden.StatusCode -ne 403 -or $classBatchesForbidden.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native candidate-account batches did not preserve the candidates.write boundary'
|
|
}
|
|
|
|
$schoolBatchState = $nativeSchoolBatches.Content | ConvertFrom-Json
|
|
$batchClass = @($schoolBatchState.classes)[0]
|
|
if ($null -eq $batchClass) {
|
|
throw 'Seed data did not provide an active class for account-batch smoke testing'
|
|
}
|
|
$batchSubmitBody = @{ quotas = @(@{ classId = $batchClass.id; count = 2 }) } | ConvertTo-Json -Depth 4 -Compress
|
|
$batchSubmitResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-account-batches" -Method Post -ContentType 'application/json' -Body $batchSubmitBody -WebSession $nativeSchoolSession
|
|
if ($batchSubmitResponse.StatusCode -ne 202 -or $batchSubmitResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native candidate-account batch submission did not use ASP.NET Core'
|
|
}
|
|
$submittedBatch = ($batchSubmitResponse.Content | ConvertFrom-Json).batch
|
|
if ($submittedBatch.status -ne 'pending' -or $submittedBatch.totalCount -ne 2 -or $submittedBatch.workflow.status -ne 'pending') {
|
|
throw 'Native candidate-account batch submission did not create its workflow atomically'
|
|
}
|
|
$schoolBatchReview = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-account-batches/$($submittedBatch.id)" -Method Patch -ContentType 'application/json' -Body (@{ status = 'approved'; reviewNote = '越权审批' } | ConvertTo-Json -Compress) -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($schoolBatchReview.StatusCode -ne 403) {
|
|
throw 'Native account-batch review did not enforce the current workflow assignee'
|
|
}
|
|
$batchApproveBody = @{ status = 'approved'; reviewNote = '原生终审通过' } | ConvertTo-Json -Compress
|
|
$batchApproveResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-account-batches/$($submittedBatch.id)" -Method Patch -ContentType 'application/json' -Body $batchApproveBody -WebSession $nativeSession
|
|
if ($batchApproveResponse.StatusCode -ne 200 -or $batchApproveResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native candidate-account batch approval did not use ASP.NET Core'
|
|
}
|
|
$approvedBatch = ($batchApproveResponse.Content | ConvertFrom-Json).batch
|
|
if ($approvedBatch.status -ne 'approved' -or @($approvedBatch.items).Count -ne 2 -or @($approvedBatch.items | Where-Object { -not $_.candidateNumber -or $_.initialPassword -notmatch '^Init-' }).Count -ne 0) {
|
|
throw 'Native candidate-account batch approval did not generate all candidate credentials'
|
|
}
|
|
$generatedAccount = @($approvedBatch.items)[0]
|
|
$generatedLoginBody = @{ username = $generatedAccount.candidateNumber; password = $generatedAccount.initialPassword } | ConvertTo-Json -Compress
|
|
$generatedLogin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $generatedLoginBody
|
|
if ($generatedLogin.user.candidateNumber -ne $generatedAccount.candidateNumber -or $generatedLogin.user.mustChangePassword -ne $true) {
|
|
throw 'Generated candidate credentials are not compatible with native authentication'
|
|
}
|
|
$duplicateBatchApproval = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/candidate-account-batches/$($submittedBatch.id)" -Method Patch -ContentType 'application/json' -Body $batchApproveBody -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($duplicateBatchApproval.StatusCode -ne 404) {
|
|
throw 'Native candidate-account batch approval was not idempotently protected'
|
|
}
|
|
|
|
$schoolCreateBody = @{
|
|
name = '原生迁移测试学校'
|
|
code = 'NATIVE_SMOKE'
|
|
address = '迁移测试路 1 号'
|
|
isSourceSchool = $true
|
|
isAdmissionSchool = $true
|
|
} | ConvertTo-Json -Compress
|
|
$schoolCreateResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/schools" -Method Post -ContentType 'application/json' -Body $schoolCreateBody -WebSession $nativeSession
|
|
if ($schoolCreateResponse.StatusCode -ne 201 -or $schoolCreateResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native school creation did not use the ASP.NET Core endpoint'
|
|
}
|
|
$createdSchool = ($schoolCreateResponse.Content | ConvertFrom-Json).school
|
|
if ($createdSchool.code -ne 'NATIVE_SMOKE' -or $createdSchool.active -ne $true) {
|
|
throw 'Native school creation returned an unexpected projection'
|
|
}
|
|
$duplicateSchool = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/schools" -Method Post -ContentType 'application/json' -Body $schoolCreateBody -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($duplicateSchool.StatusCode -ne 409) {
|
|
throw 'Native school creation did not reject a duplicate school code'
|
|
}
|
|
$schoolCreateForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/schools" -Method Post -ContentType 'application/json' -Body $schoolCreateBody -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($schoolCreateForbidden.StatusCode -ne 403 -or $schoolCreateForbidden.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native school creation did not preserve the super-admin boundary'
|
|
}
|
|
|
|
$schoolPatchBody = @{ address = '迁移测试路 2 号'; isAdmissionSchool = $false } | ConvertTo-Json -Compress
|
|
$schoolPatch = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/schools/$($createdSchool.id)" -Method Patch -ContentType 'application/json' -Body $schoolPatchBody -WebSession $nativeSession
|
|
if ($schoolPatch.school.address -ne '迁移测试路 2 号' -or $schoolPatch.school.isAdmissionSchool -ne $false) {
|
|
throw 'Native school update did not persist the requested fields'
|
|
}
|
|
|
|
$newSchoolAdminBody = @{
|
|
username = 'native_school_writer'
|
|
password = '12345678'
|
|
displayName = '原生校级管理员'
|
|
adminLevel = 'school'
|
|
schoolId = $createdSchool.id
|
|
} | ConvertTo-Json -Compress
|
|
$newSchoolAdminResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/admins" -Method Post -ContentType 'application/json' -Body $newSchoolAdminBody -WebSession $nativeSession
|
|
if ($newSchoolAdminResponse.StatusCode -ne 201 -or $newSchoolAdminResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native administrator creation did not use the ASP.NET Core endpoint'
|
|
}
|
|
$createdSchoolAdmin = ($newSchoolAdminResponse.Content | ConvertFrom-Json).admin
|
|
|
|
$newSchoolAdminSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
$newSchoolAdminLoginBody = @{ username = 'native_school_writer'; password = '12345678' } | ConvertTo-Json -Compress
|
|
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $newSchoolAdminLoginBody -WebSession $newSchoolAdminSession | Out-Null
|
|
$classCreateBody = @{ name = '迁移测试班'; grade = '2026级' } | ConvertTo-Json -Compress
|
|
$classCreateResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/classes" -Method Post -ContentType 'application/json' -Body $classCreateBody -WebSession $newSchoolAdminSession
|
|
if ($classCreateResponse.StatusCode -ne 201 -or $classCreateResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native class creation did not use the ASP.NET Core endpoint'
|
|
}
|
|
$createdClass = ($classCreateResponse.Content | ConvertFrom-Json).schoolClass
|
|
$superClassForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/classes" -Method Post -ContentType 'application/json' -Body $classCreateBody -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($superClassForbidden.StatusCode -ne 403) {
|
|
throw 'Native class creation did not preserve the school-admin boundary'
|
|
}
|
|
$classPatchBody = @{ name = '迁移测试一班'; active = $true } | ConvertTo-Json -Compress
|
|
$classPatch = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/classes/$($createdClass.id)" -Method Patch -ContentType 'application/json' -Body $classPatchBody -WebSession $newSchoolAdminSession
|
|
if ($classPatch.schoolClass.name -ne '迁移测试一班') {
|
|
throw 'Native class update did not persist the requested name'
|
|
}
|
|
|
|
$newClassAdminBody = @{
|
|
username = 'native_class_writer'
|
|
password = '12345678'
|
|
displayName = '原生班级管理员'
|
|
adminLevel = 'super'
|
|
classId = $createdClass.id
|
|
} | ConvertTo-Json -Compress
|
|
$newClassAdminResponse = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/admins" -Method Post -ContentType 'application/json' -Body $newClassAdminBody -WebSession $newSchoolAdminSession
|
|
if ($newClassAdminResponse.admin.adminLevel -ne 'class' -or $newClassAdminResponse.admin.schoolId -ne $createdSchool.id) {
|
|
throw 'School admin did not create a class-scoped administrator'
|
|
}
|
|
$createdClassAdmin = $newClassAdminResponse.admin
|
|
$newClassAdminSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
$newClassAdminLoginBody = @{ username = 'native_class_writer'; password = '12345678' } | ConvertTo-Json -Compress
|
|
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $newClassAdminLoginBody -WebSession $newClassAdminSession | Out-Null
|
|
|
|
$disableClassAdminBody = @{ active = $false; displayName = '原生班级管理员(停用)' } | ConvertTo-Json -Compress
|
|
$disabledClassAdmin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/admins/$($createdClassAdmin.id)" -Method Patch -ContentType 'application/json' -Body $disableClassAdminBody -WebSession $newSchoolAdminSession
|
|
if ($disabledClassAdmin.admin.displayName -ne '原生班级管理员(停用)') {
|
|
throw 'Native administrator update did not persist the display name'
|
|
}
|
|
$invalidatedClassSession = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/auth/me" -WebSession $newClassAdminSession -SkipHttpErrorCheck
|
|
$invalidatedClassIdentity = $invalidatedClassSession.Content | ConvertFrom-Json
|
|
if ($invalidatedClassSession.StatusCode -ne 200 -or $null -ne $invalidatedClassIdentity.user) {
|
|
throw 'Disabling an administrator did not invalidate existing sessions'
|
|
}
|
|
|
|
$resetClassAdmin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/admins/$($createdClassAdmin.id)/reset-password" -Method Post -WebSession $newSchoolAdminSession
|
|
if ($resetClassAdmin.username -ne 'native_class_writer' -or $resetClassAdmin.temporaryPassword -notmatch '^Reset-[A-Za-z0-9_-]+$') {
|
|
throw 'Native administrator password reset returned an invalid temporary password'
|
|
}
|
|
$resetLoginBody = @{ username = 'native_class_writer'; password = $resetClassAdmin.temporaryPassword } | ConvertTo-Json -Compress
|
|
$resetLogin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $resetLoginBody
|
|
if ($resetLogin.user.username -ne 'native_class_writer') {
|
|
throw 'The temporary administrator password is not compatible with native authentication'
|
|
}
|
|
|
|
$adminStateBeforeSetting = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/admins" -WebSession $nativeSession
|
|
$originalSelfRegistration = [bool]$adminStateBeforeSetting.selfRegistrationEnabled
|
|
$settingBody = @{ enabled = -not $originalSelfRegistration } | ConvertTo-Json -Compress
|
|
$settingUpdate = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/settings/self-registration" -Method Put -ContentType 'application/json' -Body $settingBody -WebSession $nativeSession
|
|
if ($settingUpdate.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or ($settingUpdate.Content | ConvertFrom-Json).enabled -eq $originalSelfRegistration) {
|
|
throw 'Native self-registration setting did not persist the requested value'
|
|
}
|
|
$settingForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/settings/self-registration" -Method Put -ContentType 'application/json' -Body $settingBody -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($settingForbidden.StatusCode -ne 403) {
|
|
throw 'Native self-registration setting did not preserve the super-admin boundary'
|
|
}
|
|
$restoreSettingBody = @{ enabled = $originalSelfRegistration } | ConvertTo-Json -Compress
|
|
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/settings/self-registration" -Method Put -ContentType 'application/json' -Body $restoreSettingBody -WebSession $nativeSession | Out-Null
|
|
|
|
$examCreateForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams" -Method Post -ContentType 'application/json' -Body '{}' -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($examCreateForbidden.StatusCode -ne 403 -or $examCreateForbidden.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native exam creation did not preserve the super-admin boundary'
|
|
}
|
|
$invalidExam = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams" -Method Post -ContentType 'application/json' -Body (@{ name = '缺少日期的考试' } | ConvertTo-Json -Compress) -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidExam.StatusCode -ne 400 -or $invalidExam.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native exam creation accepted a request without the required dates'
|
|
}
|
|
$invalidExamScoringBody = @{
|
|
name = '合格线无效的考试'
|
|
registrationStart = '2035-01-01T00:00:00.000Z'
|
|
registrationEnd = '2035-01-10T00:00:00.000Z'
|
|
examStart = '2035-02-01T00:00:00.000Z'
|
|
examEnd = '2035-02-02T00:00:00.000Z'
|
|
subjects = @(@{ name = '语文'; fullScore = 100; passRule = 'fixed_score'; passValue = 101 })
|
|
passPolicy = 'fixed_score'
|
|
passValue = 60
|
|
} | ConvertTo-Json -Depth 10 -Compress
|
|
$invalidExamScoring = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams" -Method Post -ContentType 'application/json' -Body $invalidExamScoringBody -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidExamScoring.StatusCode -ne 400) {
|
|
throw 'Native exam creation accepted an invalid subject passing score'
|
|
}
|
|
$examCode = "NATIVE-$([guid]::NewGuid().ToString('N').Substring(0, 12))"
|
|
$examCreateBody = @{
|
|
code = $examCode
|
|
name = '原生迁移考试'
|
|
description = '考试维护迁移冒烟验证'
|
|
registrationStart = '2035-01-01T00:00:00.000Z'
|
|
registrationEnd = '2035-01-10T00:00:00.000Z'
|
|
examStart = '2035-02-01T00:00:00.000Z'
|
|
examEnd = '2035-02-02T00:00:00.000Z'
|
|
location = '迁移测试考点'
|
|
passPolicy = 'fixed_score'
|
|
passValue = 130
|
|
status = 'draft'
|
|
subjects = @(
|
|
@{ name = '语文'; fullScore = 120; passRule = 'fixed_score'; passValue = 72; fee = 30 },
|
|
@{ name = '英语'; fullScore = 100; passRule = 'rank_percent'; passValue = 30; fee = 25 }
|
|
)
|
|
} | ConvertTo-Json -Depth 10 -Compress
|
|
$examCreateResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams" -Method Post -ContentType 'application/json' -Body $examCreateBody -WebSession $nativeSession
|
|
$createdExam = ($examCreateResponse.Content | ConvertFrom-Json).exam
|
|
if ($examCreateResponse.StatusCode -ne 201 -or $examCreateResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or
|
|
$createdExam.code -ne $examCode -or $createdExam.status -ne 'draft' -or @($createdExam.subjects).Count -ne 2) {
|
|
throw 'Native exam creation did not persist the requested draft and subjects'
|
|
}
|
|
$legacyExamsAfterCreate = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/exams" -WebSession $session
|
|
$nativeExamsAfterCreate = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams" -WebSession $nativeSession
|
|
Assert-JsonEquivalent -Expected $legacyExamsAfterCreate.Content -Actual $nativeExamsAfterCreate.Content -Label 'Exam creation follow-up'
|
|
|
|
$examUpdateBody = @{
|
|
name = '原生迁移考试(已修改)'
|
|
location = '迁移测试新考点'
|
|
passPolicy = 'subject_scores'
|
|
passValue = 0
|
|
subjects = @(
|
|
@{ name = '综合能力'; fullScore = 100; passRule = 'fixed_score'; passValue = 60; fee = 40; start = '13:30'; end = '15:30' }
|
|
)
|
|
} | ConvertTo-Json -Depth 10 -Compress
|
|
$examUpdateResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams/$($createdExam.id)" -Method Patch -ContentType 'application/json' -Body $examUpdateBody -WebSession $nativeSession
|
|
$updatedExam = ($examUpdateResponse.Content | ConvertFrom-Json).exam
|
|
if ($examUpdateResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $updatedExam.name -ne '原生迁移考试(已修改)' -or
|
|
$updatedExam.passPolicy -ne 'subject_scores' -or @($updatedExam.subjects).Count -ne 1 -or $updatedExam.subjects[0].start -ne '13:30') {
|
|
throw 'Native exam update did not replace the draft configuration'
|
|
}
|
|
$examPublishBody = @{ status = 'published' } | ConvertTo-Json -Compress
|
|
$examPublishResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams/$($createdExam.id)" -Method Patch -ContentType 'application/json' -Body $examPublishBody -WebSession $nativeSession
|
|
if (($examPublishResponse.Content | ConvertFrom-Json).exam.status -ne 'published') {
|
|
throw 'Native exam update did not publish the draft'
|
|
}
|
|
$publishedExamEdit = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams/$($createdExam.id)" -Method Patch -ContentType 'application/json' -Body (@{ name = '不应保存的名称' } | ConvertTo-Json -Compress) -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($publishedExamEdit.StatusCode -ne 409) {
|
|
throw 'Native exam update allowed detail changes while published'
|
|
}
|
|
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/admin/exams/$($createdExam.id)" -Method Patch -ContentType 'application/json' -Body (@{ status = 'draft' } | ConvertTo-Json -Compress) -WebSession $nativeSession | Out-Null
|
|
$examArchiveResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams/$($createdExam.id)/archive" -Method Post -WebSession $nativeSession
|
|
$archivedExam = ($examArchiveResponse.Content | ConvertFrom-Json).exam
|
|
if ($examArchiveResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $archivedExam.status -ne 'closed' -or
|
|
$archivedExam.registrationState -ne 'archived' -or -not $archivedExam.archivedAt -or $archivedExam.totalScore -ne 100) {
|
|
throw 'Native exam archive did not permanently close the exam'
|
|
}
|
|
$duplicateExamArchive = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams/$($createdExam.id)/archive" -Method Post -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($duplicateExamArchive.StatusCode -ne 409) {
|
|
throw 'Native exam archive did not reject a duplicate archive operation'
|
|
}
|
|
$archivedExamUpdate = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams/$($createdExam.id)" -Method Patch -ContentType 'application/json' -Body (@{ status = 'draft' } | ConvertTo-Json -Compress) -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($archivedExamUpdate.StatusCode -ne 409) {
|
|
throw 'Native exam update did not keep archived exams locked'
|
|
}
|
|
$archivedExamArrangement = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams/$($createdExam.id)/admission-arrangement/preview" -Method Post -ContentType 'application/json' -Body $arrangementBody -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($archivedExamArrangement.StatusCode -ne 409) {
|
|
throw 'Native arrangement preview did not keep archived exams locked'
|
|
}
|
|
$legacyExamsAfterArchive = Invoke-WebRequest -Uri "$legacyBaseUrl/api/admin/exams" -WebSession $session
|
|
$nativeExamsAfterArchive = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/exams" -WebSession $nativeSession
|
|
Assert-JsonEquivalent -Expected $legacyExamsAfterArchive.Content -Actual $nativeExamsAfterArchive.Content -Label 'Exam archive follow-up'
|
|
|
|
$noticeCreateForbidden = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/notices" -Method Post -ContentType 'application/json' -Body (@{ title = '越权公告'; content = '<p>无权发布</p>' } | ConvertTo-Json -Compress) -WebSession $nativeSchoolSession -SkipHttpErrorCheck
|
|
if ($noticeCreateForbidden.StatusCode -ne 403 -or $noticeCreateForbidden.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native notice creation did not preserve the super-admin boundary'
|
|
}
|
|
$invalidNotice = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/notices" -Method Post -ContentType 'application/json' -Body (@{ title = '无正文公告'; content = '<script>alert(1)</script>' } | ConvertTo-Json -Compress) -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($invalidNotice.StatusCode -ne 400 -or $invalidNotice.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native notice creation accepted content that became empty after sanitization'
|
|
}
|
|
$noticeCreateBody = @{
|
|
title = '原生迁移公告'
|
|
summary = ''
|
|
content = '<p>公告<strong>正文</strong><script>alert(1)</script></p><a href="javascript:alert(1)">危险链接</a>'
|
|
category = '迁移公告'
|
|
pinned = $true
|
|
status = 'draft'
|
|
} | ConvertTo-Json -Compress
|
|
$noticeCreateResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/notices" -Method Post -ContentType 'application/json' -Body $noticeCreateBody -WebSession $nativeSession
|
|
$createdNativeNotice = ($noticeCreateResponse.Content | ConvertFrom-Json).notice
|
|
if ($noticeCreateResponse.StatusCode -ne 201 -or $noticeCreateResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
|
throw 'Native notice creation did not use ASP.NET Core'
|
|
}
|
|
if ($createdNativeNotice.status -ne 'draft' -or $null -ne $createdNativeNotice.publishAt -or $createdNativeNotice.summary -ne '公告正文 危险链接') {
|
|
throw 'Native notice creation did not preserve draft state or derive its summary'
|
|
}
|
|
if ($createdNativeNotice.content -match '(?i)<script|javascript:') {
|
|
throw 'Native notice creation returned unsafe HTML'
|
|
}
|
|
$noticePublishBody = @{ title = '原生迁移公告(已发布)'; status = 'published' } | ConvertTo-Json -Compress
|
|
$noticePublishResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/admin/notices/$($createdNativeNotice.id)" -Method Patch -ContentType 'application/json' -Body $noticePublishBody -WebSession $nativeSession
|
|
$publishedNativeNotice = ($noticePublishResponse.Content | ConvertFrom-Json).notice
|
|
if ($noticePublishResponse.StatusCode -ne 200 -or $noticePublishResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $publishedNativeNotice.status -ne 'published' -or -not $publishedNativeNotice.publishAt) {
|
|
throw 'Native notice update did not publish the notice'
|
|
}
|
|
$nativePublicNoticeResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/public/notices/$($createdNativeNotice.id)"
|
|
$nativePublicNotice = ($nativePublicNoticeResponse.Content | ConvertFrom-Json).notice
|
|
if ($nativePublicNoticeResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core' -or $nativePublicNotice.title -ne '原生迁移公告(已发布)') {
|
|
throw 'Native public notice endpoint could not read the newly published notice'
|
|
}
|
|
if ($nativePublicNotice.content -match '(?i)<script|javascript:') {
|
|
throw 'Native public notice endpoint exposed unsafe HTML from an administrative write'
|
|
}
|
|
|
|
$adminCandidateRoute = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/candidate/profile" -WebSession $nativeSession -SkipHttpErrorCheck
|
|
if ($adminCandidateRoute.StatusCode -ne 403) {
|
|
throw 'Native candidate API did not enforce the candidate role boundary'
|
|
}
|
|
|
|
$nativeMe = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/me" -WebSession $nativeSession
|
|
if ($nativeMe.user.username -ne 'admin' -or $nativeMe.permissions[0] -ne '*') {
|
|
throw 'Native authentication did not preserve the session or administrator projection'
|
|
}
|
|
|
|
$totpSetupBody = @{ currentPassword = '12345678' } | ConvertTo-Json -Compress
|
|
$totpSetup = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/totp/setup" -Method Post -ContentType 'application/json' -Body $totpSetupBody -WebSession $nativeSession
|
|
if ($totpSetup.qrCode -notmatch '^data:image/png;base64,' -or $totpSetup.uri -notmatch '^otpauth://totp/') {
|
|
throw 'Native TOTP setup did not return a local PNG QR code and otpauth URI'
|
|
}
|
|
|
|
$totpCodeProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @(
|
|
'tests/helpers/current-totp-code.mjs', $totpSetup.secret
|
|
) -Environment $nodeEnvironment
|
|
if (-not $totpCodeProcess.WaitForExit(10000)) {
|
|
$totpCodeProcess.Kill($true)
|
|
throw 'Timed out while generating the native TOTP smoke-test code'
|
|
}
|
|
$totpCode = $totpCodeProcess.StandardOutput.ReadToEnd().Trim()
|
|
$totpCodeError = $totpCodeProcess.StandardError.ReadToEnd()
|
|
if ($totpCodeProcess.ExitCode -ne 0 -or -not $totpCode) {
|
|
throw "Could not generate the native TOTP smoke-test code`n$totpCodeError"
|
|
}
|
|
$totpCodeProcess.Dispose()
|
|
|
|
$totpEnableBody = @{ code = $totpCode } | ConvertTo-Json -Compress
|
|
$totpEnable = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/totp/enable" -Method Post -ContentType 'application/json' -Body $totpEnableBody -WebSession $nativeSession
|
|
if ($totpEnable.user.totpEnabled -ne $true -or $totpEnable.recoveryCodes.Count -ne 8) {
|
|
throw 'Native TOTP enablement did not persist security state or issue eight recovery codes'
|
|
}
|
|
|
|
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/logout" -Method Post -WebSession $nativeSession | Out-Null
|
|
$totpPasswordLogin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $loginBody
|
|
if ($totpPasswordLogin.requiresTotp -ne $true -or -not $totpPasswordLogin.challenge) {
|
|
throw 'Native password login bypassed enabled TOTP'
|
|
}
|
|
|
|
$totpLoginSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
|
$recoveryLoginBody = @{ challenge = $totpPasswordLogin.challenge; code = $totpEnable.recoveryCodes[0] } | ConvertTo-Json -Compress
|
|
$totpLogin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login/totp" -Method Post -ContentType 'application/json' -Body $recoveryLoginBody -WebSession $totpLoginSession
|
|
if ($totpLogin.usedRecoveryCode -ne $true -or $totpLogin.user.username -ne 'admin') {
|
|
throw 'Native TOTP recovery-code login did not create a session'
|
|
}
|
|
|
|
$totpStatus = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/totp" -WebSession $totpLoginSession
|
|
if ($totpStatus.enabled -ne $true -or $totpStatus.recoveryCodesRemaining -ne 7) {
|
|
throw 'Native TOTP recovery code was not consumed exactly once'
|
|
}
|
|
|
|
$disableBody = @{ currentPassword = '12345678'; code = $totpEnable.recoveryCodes[1] } | ConvertTo-Json -Compress
|
|
$disabled = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/totp/disable" -Method Post -ContentType 'application/json' -Body $disableBody -WebSession $totpLoginSession
|
|
if ($disabled.user.totpEnabled -ne $false) {
|
|
throw 'Native TOTP disable endpoint did not clear the security state'
|
|
}
|
|
|
|
$changePasswordBody = @{ currentPassword = '12345678'; newPassword = 'MigrationAuth456!' } | ConvertTo-Json -Compress
|
|
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/change-password" -Method Post -ContentType 'application/json' -Body $changePasswordBody -WebSession $totpLoginSession | Out-Null
|
|
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/logout" -Method Post -WebSession $totpLoginSession | Out-Null
|
|
$changedLoginBody = @{ username = 'admin'; password = 'MigrationAuth456!' } | ConvertTo-Json -Compress
|
|
$changedLogin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $changedLoginBody
|
|
if ($changedLogin.user.username -ne 'admin') {
|
|
throw 'Native password change did not create a Node-compatible PBKDF2 hash'
|
|
}
|
|
|
|
[pscustomobject]@{
|
|
AspNetCoreHost = 'passed'
|
|
StaticAssets = 'passed'
|
|
JsonProxy = 'passed'
|
|
SessionCookie = 'passed'
|
|
NativePublicApi = 'passed'
|
|
PublicParity = 'passed'
|
|
DocumentCodes = 'passed'
|
|
NativeAuthentication = 'passed'
|
|
NativeCandidateReads = 'passed'
|
|
NativeCandidateWrites = 'passed'
|
|
NativeCandidateDocuments = 'passed'
|
|
NativeCandidateAdmissions = 'passed'
|
|
NativeAdminReads = 'passed'
|
|
NativeAdminOrganizationWrites = 'passed'
|
|
NativeAdminAccountBatches = 'passed'
|
|
NativeAdminConfiguration = 'passed'
|
|
NativeAdminNoticeManagement = 'passed'
|
|
NativeAdminCenters = 'passed'
|
|
NativeAdminOperationalReads = 'passed'
|
|
NativeAdminCandidateManagement = 'passed'
|
|
NativeAdminRegistrationPaymentWrites = 'passed'
|
|
NativeAdminWorkflowOperations = 'passed'
|
|
NativeAdminExamManagement = 'passed'
|
|
NativeAdminArrangements = 'passed'
|
|
NativeAdminResults = 'passed'
|
|
NativeAdminAdmissions = 'passed'
|
|
} | Format-List
|
|
}
|
|
finally {
|
|
foreach ($process in @($nativeAuthProcess, $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($smokeArtifactRoot, [StringComparison]::OrdinalIgnoreCase) -or
|
|
-not ([System.IO.Path]::GetFileName($resolvedTestDirectory)).StartsWith('eis-migration-smoke-', [StringComparison]::Ordinal)) {
|
|
throw "Refusing to remove unexpected smoke-test directory: $resolvedTestDirectory"
|
|
}
|
|
for ($attempt = 1; $attempt -le 20; $attempt++) {
|
|
try {
|
|
Remove-Item -LiteralPath $resolvedTestDirectory -Recurse -Force -ErrorAction Stop
|
|
break
|
|
}
|
|
catch {
|
|
if ($attempt -eq 20) {
|
|
throw
|
|
}
|
|
Start-Sleep -Milliseconds 200
|
|
}
|
|
}
|
|
}
|
|
}
|