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

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:
biss committed 2026-07-22 19:45:22 +08:00
1 parent ae472aabb0
commit 07efa6813b
11 files changed
+1171 -3

No files matched your search

+2
View File
@@ -23,6 +23,8 @@ PORT=4173
# 原生 ASP.NET Core 认证切换。迁移期间默认关闭;生产环境开启时必须配置共享 Redis, # 原生 ASP.NET Core 认证切换。迁移期间默认关闭;生产环境开启时必须配置共享 Redis,
# 以便尚未迁移的 Node 受保护接口识别由 ASP.NET Core 创建的会话。 # 以便尚未迁移的 Node 受保护接口识别由 ASP.NET Core 创建的会话。
AUTH_NATIVE_ENABLED=false AUTH_NATIVE_ENABLED=false
# 第一批考生只读接口切换;必须与 AUTH_NATIVE_ENABLED=true 同时使用。
CANDIDATE_NATIVE_ENABLED=false
# 仅在首次创建空数据库时使用。部署前务必修改初始密码。 # 仅在首次创建空数据库时使用。部署前务必修改初始密码。
INITIAL_ADMIN_USERNAME=admin INITIAL_ADMIN_USERNAME=admin
+7
View File
@@ -47,6 +47,13 @@ $env:AUTH_NATIVE_ENABLED = 'true'
开发环境未配置 Redis 时可以使用进程内状态独立验证原生认证。生产环境以及仍需访问 Node 受保护接口的联调环境必须配置 `REDIS_URL` 或 `REDIS_SESSION_URL`;两个运行时会复用相同逻辑库和 `exam-information:auth` 键前缀,从而共享登录会话。`GET /health/migration` 会报告 `authentication.nativeEnabled`、状态后端和跨运行时会话共享能力。 开发环境未配置 Redis 时可以使用进程内状态独立验证原生认证。生产环境以及仍需访问 Node 受保护接口的联调环境必须配置 `REDIS_URL` 或 `REDIS_SESSION_URL`;两个运行时会复用相同逻辑库和 `exam-information:auth` 键前缀,从而共享登录会话。`GET /health/migration` 会报告 `authentication.nativeEnabled`、状态后端和跨运行时会话共享能力。
考生域第一批只读端点(首页、通知、个人资料、可报名考试、我的报名)可通过以下开关原生运行;该开关必须与原生认证及共享 Redis 同时启用。资料更新、报名提交、成绩、准考证、复议和志愿填报仍会继续转发给 Node:
```powershell
$env:AUTH_NATIVE_ENABLED = 'true'
$env:CANDIDATE_NATIVE_ENABLED = 'true'
```
完整的宿主、静态资源、JSON 转发和 Session Cookie 冒烟测试: 完整的宿主、静态资源、JSON 转发和 Session Cookie 冒烟测试:
```powershell ```powershell
+104 -1
View File
@@ -84,6 +84,74 @@ function Wait-ForUrl {
throw "Timed out waiting for $Uri" 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 { try {
New-Item -ItemType Directory -Path $testDirectory | Out-Null New-Item -ItemType Directory -Path $testDirectory | Out-Null
@@ -308,6 +376,8 @@ try {
) -Environment @{ ) -Environment @{
ASPNETCORE_ENVIRONMENT = 'Development' ASPNETCORE_ENVIRONMENT = 'Development'
AUTH_NATIVE_ENABLED = 'true' AUTH_NATIVE_ENABLED = 'true'
CANDIDATE_NATIVE_ENABLED = 'true'
CANDIDATE_NATIVE_ALLOW_MEMORY = 'true'
LegacyNode__Enabled = 'true' LegacyNode__Enabled = 'true'
LegacyNode__BaseUrl = $legacyBaseUrl LegacyNode__BaseUrl = $legacyBaseUrl
DATABASE_CLIENT = 'sqlite' DATABASE_CLIENT = 'sqlite'
@@ -319,6 +389,25 @@ try {
} }
Wait-ForUrl -Uri "$nativeAuthBaseUrl/health/live" -Processes @($nodeProcess, $nativeAuthProcess) 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] $registrationSchool = @($homePayload.schools | Select-Object -First 1)[0]
$registrationClass = @($homePayload.classes | Where-Object schoolId -eq $registrationSchool.id | 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) { if ($null -eq $registrationSchool -or $null -eq $registrationClass) {
@@ -337,10 +426,19 @@ try {
} }
$registration = $registrationResponse.Content | ConvertFrom-Json $registration = $registrationResponse.Content | ConvertFrom-Json
$registeredLoginBody = @{ username = $registration.registrationNumber; password = 'Registration456!' } | ConvertTo-Json -Compress $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) { if ($registeredLogin.user.candidateNumber -ne $registration.registrationNumber) {
throw 'Native self-registration did not create a usable candidate account' 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() $nativeSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
$nativeLoginResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $loginBody -WebSession $nativeSession $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') { if ($nativeLogin.ok -ne $true -or $nativeLogin.user.username -ne 'admin') {
throw 'Native authentication could not verify the existing Node PBKDF2 account' 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 $nativeMe = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/me" -WebSession $nativeSession
if ($nativeMe.user.username -ne 'admin' -or $nativeMe.permissions[0] -ne '*') { if ($nativeMe.user.username -ne 'admin' -or $nativeMe.permissions[0] -ne '*') {
@@ -425,6 +527,7 @@ try {
PublicParity = 'passed' PublicParity = 'passed'
DocumentCodes = 'passed' DocumentCodes = 'passed'
NativeAuthentication = 'passed' NativeAuthentication = 'passed'
NativeCandidateReads = 'passed'
} | Format-List } | Format-List
} }
finally { finally {
Loaded 3 of 11 files, more files were not shown because too many files have changed in this diff. Show more