Files
jiapuapp/tests/auth-tac-openapi-contract.ps1
T
2026-07-23 08:24:07 +08:00

129 lines
7.5 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$json = Get-Content -LiteralPath (Join-Path $root 'APP.openapi.json') -Raw -Encoding UTF8 | ConvertFrom-Json
$yaml = Get-Content -LiteralPath (Join-Path $root 'APP.openapi.yaml') -Raw -Encoding UTF8
$blockers = [System.Collections.Generic.List[string]]::new()
function Require-Operation {
param([string]$Path, [string]$Method)
$pathProperty = $json.paths.PSObject.Properties[$Path]
if ($null -eq $pathProperty -or $null -eq $pathProperty.Value.PSObject.Properties[$Method]) {
throw "认证源合同缺少操作:$($Method.ToUpperInvariant()) $Path"
}
if ($yaml -notmatch [regex]::Escape(" $Path`:") -or $yaml -notmatch "(?m)^ $Method`:\s*$") {
throw "YAML 认证源合同缺少操作:$($Method.ToUpperInvariant()) $Path"
}
return $pathProperty.Value.PSObject.Properties[$Method].Value
}
function Require-Schema {
param([string]$Name)
$property = $json.components.schemas.PSObject.Properties[$Name]
if ($null -eq $property) { throw "认证源合同缺少 schema$Name" }
if ($yaml -notmatch "(?m)^ $([regex]::Escape($Name)):\s*$") { throw "YAML 认证源合同缺少 schema$Name" }
return $property.Value
}
function Assert-ExactSet {
param([object[]]$Actual, [object[]]$Expected, [string]$Label)
$actualSet = @($Actual | ForEach-Object { [string]$_ } | Sort-Object -Unique)
$expectedSet = @($Expected | ForEach-Object { [string]$_ } | Sort-Object -Unique)
if (($actualSet -join ',') -ne ($expectedSet -join ',')) {
throw "$Label 漂移:actual=[$($actualSet -join ',')] expected=[$($expectedSet -join ',')]"
}
}
$operations = @(
@('/captcha/require', 'get'),
@('/captcha/challenge', 'post'),
@('/captcha/verify', 'post'),
@('/genealogy/app/auth/sms/code', 'post'),
@('/genealogy/app/auth/login', 'post'),
@('/genealogy/app/auth/login/sms', 'post'),
@('/genealogy/app/auth/register', 'post'),
@('/genealogy/app/auth/password/reset', 'put')
)
foreach ($entry in $operations) { [void](Require-Operation -Path $entry[0] -Method $entry[1]) }
$expectedVerificationScenes = @('APP_SMS_LOGIN', 'APP_REGISTER', 'APP_FORGOT_PASSWORD', 'APP_PHONE_CHANGE', 'APP_ACCOUNT_DEACTIVATE')
foreach ($schemaName in @('VerificationChallengeBody', 'VerificationCheckBody')) {
$schema = Require-Schema -Name $schemaName
Assert-ExactSet -Actual @($schema.properties.sceneCode.enum) -Expected $expectedVerificationScenes -Label "$schemaName.sceneCode"
}
$check = Require-Schema -Name 'VerificationCheckBody'
Assert-ExactSet -Actual @($check.properties.payload.oneOf.'$ref') -Expected @('#/components/schemas/TianaiVerificationPayload', '#/components/schemas/SystemImageVerificationPayload') -Label 'VerificationCheckBody.payload.oneOf'
$requiredCheckFields = @('tenantId', 'clientId', 'sceneCode', 'subject', 'challengeId', 'providerCode', 'captchaType', 'payload')
$missingCheckFields = @($requiredCheckFields | Where-Object { $_ -notin @($check.required) })
if ($missingCheckFields.Count -gt 0) {
$blockers.Add("VerificationCheckBody 未强制字段:$($missingCheckFields -join '、')。")
}
if ($check.additionalProperties -ne $false) {
$blockers.Add('VerificationCheckBody 未设置 additionalProperties=false,服务端校验边界仍可接受未声明字段。')
}
if (@($check.oneOf).Count -lt 2 -or $check.discriminator.propertyName -ne 'providerCode') {
$blockers.Add('VerificationCheckBody 未用 providerCode 判别至少两个 oneOf 分支,providerCode、captchaType 与 payload 形态无法被原子约束。')
}
$tianaiPayload = Require-Schema -Name 'TianaiVerificationPayload'
Assert-ExactSet -Actual @($tianaiPayload.required) -Expected @('track') -Label 'TianaiVerificationPayload.required'
if ($tianaiPayload.properties.track.'$ref' -ne '#/components/schemas/TianaiCaptchaTrack') { throw '天爱校验载荷必须唯一包装为 payload.track' }
if ($tianaiPayload.additionalProperties -ne $false) {
$blockers.Add('TianaiVerificationPayload 未设置 additionalProperties=false,历史直传字段仍可能绕过 payload.track 约束。')
}
$systemImagePayload = Require-Schema -Name 'SystemImageVerificationPayload'
if ($systemImagePayload.additionalProperties -ne $false) {
$blockers.Add('SystemImageVerificationPayload 未设置 additionalProperties=false,系统图形验证码载荷边界未闭合。')
}
$track = Require-Schema -Name 'TianaiCaptchaTrack'
Assert-ExactSet -Actual @($track.required) -Expected @('bgImageWidth', 'bgImageHeight', 'startTime', 'stopTime', 'trackList') -Label 'TianaiCaptchaTrack.required'
if ($track.properties.trackList.minItems -ne 1) { throw '天爱行为轨迹不得为空' }
$smsCode = Require-Schema -Name 'SmsCodeBody'
Assert-ExactSet -Actual @($smsCode.required) -Expected @('clientId', 'grantType', 'tenantId', 'sceneCode', 'phone', 'validToken') -Label 'SmsCodeBody.required'
if ($smsCode.additionalProperties -ne $false) { throw 'SmsCodeBody 必须拒绝历史供应商字段' }
$expectedPublicSmsScenes = @('APP_SMS_LOGIN', 'APP_REGISTER', 'APP_FORGOT_PASSWORD', 'APP_ACCOUNT_DEACTIVATE')
$actualPublicSmsScenes = @($smsCode.properties.sceneCode.enum | ForEach-Object { [string]$_ } | Sort-Object -Unique)
$expectedPublicSmsScenes = @($expectedPublicSmsScenes | Sort-Object -Unique)
if (($actualPublicSmsScenes -join ',') -ne ($expectedPublicSmsScenes -join ',')) {
$blockers.Add('公共 SmsCodeBody.sceneCode 必须删除 APP_PHONE_CHANGE;换绑发码只能由需要 SaToken 的专用 /auth/phone/sms/code operation 持有。')
}
$smsSecretProperty = $json.components.schemas.PSObject.Properties['SmsCodeSecret']
if ($null -eq $smsSecretProperty) {
$blockers.Add('缺少全认证场景共用的 SmsCodeSecret;当前四位码必须原子升级为严格六位 ASCII 数字,不能保留 4/6 双接受。')
} else {
$smsSecret = $smsSecretProperty.Value
if ($smsSecret.type -ne 'string' -or $smsSecret.writeOnly -ne $true -or
[int]$smsSecret.minLength -ne 6 -or [int]$smsSecret.maxLength -ne 6 -or
[string]$smsSecret.pattern -ne '^[0-9]{6}$' -or $smsSecret.example) {
$blockers.Add('SmsCodeSecret 必须是无示例、保留前导零的 writeOnly 六位 ASCII 数字字符串。')
}
}
foreach ($schemaName in @('SmsLoginBody', 'PasswordRegisterBody', 'PasswordResetBody')) {
$schema = Require-Schema -Name $schemaName
$actualRef = [string]$schema.properties.smsCode.'$ref'
if ($actualRef -ne '#/components/schemas/SmsCodeSecret') {
$blockers.Add("$schemaName.smsCode 必须引用唯一 SmsCodeSecret,禁止继续内联四位码或接受双长度。")
}
}
$passwordLogin = Require-Schema -Name 'PasswordLoginBody'
$passwordFields = @($passwordLogin.properties.PSObject.Properties.Name)
$passwordRequired = @($passwordLogin.required)
if ('validToken' -notin $passwordFields -or 'validToken' -notin $passwordRequired) {
$blockers.Add('PasswordLoginBody 未定义并强制消费 validToken,密码登录无法形成服务端 TAC 闭环,客户端先滑后登录仍可被绕过。')
}
if ($blockers.Count -gt 0) {
$details = $blockers | ForEach-Object { "- $_" }
throw (@(
'AUTH-TAC-OPENAPI-CONTRACT BLOCKED'
$details
'- 关闭条件:后端同步更新同版本 JSON/YAML;校验体按 providerCode 严格区分供应商并拒绝缺字段/多余字段;密码登录原子消费绑定租户、客户端、场景、手机号的一次性 TAC 票据;全活动短信码原子迁移为六位;APP_PHONE_CHANGE 改由专用受保护发码 operation;全部部署到 HTTPS 环境并通过反向用例。'
) -join [Environment]::NewLine)
}
Write-Output 'AUTH-TAC-OPENAPI-CONTRACT PASS'