已完成考生域第一批只读迁移:
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:
1 parent
ae472aabb0
commit
07efa6813b
11 files changed
+1171
-3
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
Reference in new issue
Block a user