已完成考生域第一批只读迁移:

GET /api/candidate/dashboard
GET /api/candidate/notices
GET /api/candidate/profile
GET /api/candidate/exams
GET /api/candidate/registrations
This commit is contained in:
2026-07-22 19:45:22 +08:00 Unverified
parent ae472aabb0
commit 07efa6813b
11 changed files with 1171 additions and 3 deletions
+104 -1
View File
@@ -84,6 +84,74 @@ function Wait-ForUrl {
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 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
@@ -308,6 +376,8 @@ try {
) -Environment @{
ASPNETCORE_ENVIRONMENT = 'Development'
AUTH_NATIVE_ENABLED = 'true'
CANDIDATE_NATIVE_ENABLED = 'true'
CANDIDATE_NATIVE_ALLOW_MEMORY = 'true'
LegacyNode__Enabled = 'true'
LegacyNode__BaseUrl = $legacyBaseUrl
DATABASE_CLIENT = 'sqlite'
@@ -319,6 +389,25 @@ try {
}
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'"
}
$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) {
@@ -337,10 +426,19 @@ try {
}
$registration = $registrationResponse.Content | ConvertFrom-Json
$registeredLoginBody = @{ username = $registration.registrationNumber; password = 'Registration456!' } | ConvertTo-Json -Compress
$registeredLogin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $registeredLoginBody
$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'
}
$nativeSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
$nativeLoginResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $loginBody -WebSession $nativeSession
@@ -351,6 +449,10 @@ try {
if ($nativeLogin.ok -ne $true -or $nativeLogin.user.username -ne 'admin') {
throw 'Native authentication could not verify the existing Node PBKDF2 account'
}
$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 '*') {
@@ -425,6 +527,7 @@ try {
PublicParity = 'passed'
DocumentCodes = 'passed'
NativeAuthentication = 'passed'
NativeCandidateReads = 'passed'
} | Format-List
}
finally {