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

142 lines
5.2 KiB
PowerShell

$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$document = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'APP.openapi.json') | ConvertFrom-Json
$yaml = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'APP.openapi.yaml')
$issues = New-Object System.Collections.Generic.List[string]
function Add-Issue {
param([string]$Message)
$script:issues.Add($Message)
}
function Get-Schema {
param([string]$Name)
$property = $document.components.schemas.PSObject.Properties[$Name]
if (-not $property) {
Add-Issue "JSON missing schema owner: $Name"
return $null
}
return $property.Value
}
function Assert-Required {
param([object]$Schema, [string]$SchemaName, [string[]]$Fields)
if (-not $Schema) { return }
foreach ($field in $Fields) {
if ($field -notin @($Schema.required)) {
Add-Issue "JSON $SchemaName.required missing: $field"
}
}
}
$path = '/genealogy/app/auth/profile'
$pathProperty = $document.paths.PSObject.Properties[$path]
$operation = if ($pathProperty) { $pathProperty.Value.get } else { $null }
if (-not $operation) {
Add-Issue "JSON missing GET $path"
} else {
$hasSaToken = $false
foreach ($securityRequirement in @($operation.security)) {
if ($securityRequirement.PSObject.Properties.Name -contains 'SaToken') { $hasSaToken = $true }
}
if (-not $hasSaToken) { Add-Issue "JSON GET $path must require SaToken" }
$response = $operation.responses.PSObject.Properties['200'].Value
if ($response.'$ref') {
$responseName = ([string]$response.'$ref').Split('/')[-1]
$response = $document.components.responses.PSObject.Properties[$responseName].Value
}
$media = @($response.content.PSObject.Properties)
$responseRef = if ($media.Count -gt 0) { [string]$media[0].Value.schema.'$ref' } else { '' }
if ($responseRef -ne '#/components/schemas/RAppProfileVo') {
Add-Issue "JSON GET $path must return RAppProfileVo; actual: $responseRef"
}
}
$envelope = Get-Schema 'RAppProfileVo'
$profile = Get-Schema 'AppProfileVo'
Assert-Required $envelope 'RAppProfileVo' @('code', 'data')
Assert-Required $profile 'AppProfileVo' @('phone')
if ($envelope) {
if ($envelope.properties.code.type -ne 'integer') {
Add-Issue 'JSON RAppProfileVo.code must be integer'
}
if ($envelope.properties.data.'$ref' -ne '#/components/schemas/AppProfileVo') {
Add-Issue 'JSON RAppProfileVo.data must reference AppProfileVo'
}
}
if ($profile) {
foreach ($field in @('phone', 'nickName', 'realName', 'email')) {
$property = $profile.properties.PSObject.Properties[$field]
if (-not $property) {
Add-Issue "JSON AppProfileVo missing property: $field"
} elseif ($property.Value.type -ne 'string') {
Add-Issue "JSON AppProfileVo.$field must be string"
}
}
$phonePattern = [string]$profile.properties.phone.pattern
try {
$phoneRegex = [regex]::new($phonePattern)
if (-not $phonePattern -or
-not $phoneRegex.IsMatch('13800138000') -or
$phoneRegex.IsMatch('138****8000') -or
$phoneRegex.IsMatch('+8613800138000')) {
Add-Issue 'JSON AppProfileVo.phone must declare the canonical mainland mobile pattern'
}
} catch {
Add-Issue 'JSON AppProfileVo.phone pattern must be a valid regular expression'
}
foreach ($field in @('nickName', 'realName')) {
if ([int]$profile.properties.$field.minLength -lt 1) {
Add-Issue "JSON AppProfileVo.$field must declare minLength >= 1 when present"
}
if ([int]$profile.properties.$field.maxLength -ne 30) {
Add-Issue "JSON AppProfileVo.$field must declare maxLength 30"
}
}
if ([int]$profile.properties.email.minLength -lt 1) {
Add-Issue 'JSON AppProfileVo.email must declare minLength >= 1 when present'
}
if ([int]$profile.properties.email.maxLength -ne 100) {
Add-Issue 'JSON AppProfileVo.email must declare maxLength 100'
}
if ($profile.properties.email.format -ne 'email') {
Add-Issue 'JSON AppProfileVo.email must declare email format'
}
}
foreach ($yamlFact in @(
' /genealogy/app/auth/profile:',
'#/components/schemas/RAppProfileVo',
' RAppProfileVo:',
' AppProfileVo:',
' - code',
' - data',
' - phone',
' nickName:',
' realName:',
' email:'
)) {
if (-not $yaml.Contains($yamlFact)) {
Add-Issue "YAML fact is missing: $yamlFact"
}
}
if ($issues.Count -gt 0) {
$lines = New-Object System.Collections.Generic.List[string]
$lines.Add('PROFILE-OPENAPI-CONTRACT BLOCKED')
foreach ($issue in $issues) { $lines.Add("- $issue") }
$lines.Add('- Only phone is required on the wire. Missing optional profile fields mean unset; present empty or null values are invalid.')
$lines.Add('- The first client adapter immediately replaces raw phone with maskedPhone and projects nickName, realName, and email; numeric userId/avatar are not consumed.')
$lines.Add('- Authenticated release tests must still cover masked display, account switching, 401, malformed data, 5xx, timeout, and cancellation.')
$lines.Add('- Replace both protected exports from one backend version; do not hand-edit APP.openapi.json or APP.openapi.yaml.')
throw ($lines -join [Environment]::NewLine)
}
Write-Output 'PROFILE-OPENAPI-CONTRACT PASS'