This commit is contained in:
+19
-4
@@ -3,26 +3,41 @@ $PSNativeCommandUseErrorActionPreference = $true
|
||||
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $projectRoot
|
||||
|
||||
function Assert-NativeSuccess {
|
||||
param([Parameter(Mandatory = $true)][string]$Operation)
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Operation failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
|
||||
$unformatted = gofmt -l cmd internal
|
||||
if ($unformatted) {
|
||||
throw "The following Go files are not formatted:`n$($unformatted -join "`n")"
|
||||
}
|
||||
|
||||
go vet ./...
|
||||
# Database-backed packages share the seeded local tenant. Run packages
|
||||
# sequentially whenever integration variables are present so temporary SaaS
|
||||
# fixtures cannot become visible to identity assertions in another package.
|
||||
go test -p 1 '-coverprofile=coverage.out' ./...
|
||||
Assert-NativeSuccess -Operation 'go vet'
|
||||
# Unit tests stay independent from local service state. Database-backed suites
|
||||
# are executed by test-integration.ps1 against a disposable migrated database.
|
||||
go test '-coverprofile=coverage.out' ./...
|
||||
Assert-NativeSuccess -Operation 'go test'
|
||||
|
||||
& (Join-Path $PSScriptRoot 'test-integration.ps1')
|
||||
|
||||
Push-Location web
|
||||
try {
|
||||
npm ci
|
||||
Assert-NativeSuccess -Operation 'npm ci'
|
||||
npm run generate:api
|
||||
Assert-NativeSuccess -Operation 'OpenAPI client generation'
|
||||
npm run typecheck
|
||||
Assert-NativeSuccess -Operation 'web typecheck'
|
||||
npm run build
|
||||
Assert-NativeSuccess -Operation 'web build'
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
docker compose config --quiet
|
||||
Assert-NativeSuccess -Operation 'docker compose config'
|
||||
Write-Host 'All local checks passed.'
|
||||
|
||||
+3
-3
@@ -5,12 +5,12 @@ Set-Location -LiteralPath $projectRoot
|
||||
docker compose config --quiet
|
||||
docker compose up --build -d
|
||||
|
||||
Write-Host '等待 API 就绪...'
|
||||
Write-Host 'Waiting for API readiness...'
|
||||
for ($attempt = 1; $attempt -le 60; $attempt++) {
|
||||
try {
|
||||
$response = Invoke-RestMethod -Uri 'http://localhost:58080/health/ready' -TimeoutSec 2
|
||||
if ($response.data.status -eq 'ready') {
|
||||
Write-Host '本地环境已就绪:'
|
||||
Write-Host 'Local environment is ready:'
|
||||
Write-Host ' Web http://localhost:3100'
|
||||
Write-Host ' API http://localhost:58080'
|
||||
Write-Host ' Callbacks http://localhost:58081'
|
||||
@@ -25,4 +25,4 @@ for ($attempt = 1; $attempt -le 60; $attempt++) {
|
||||
}
|
||||
|
||||
docker compose ps
|
||||
throw 'API在约定时间内未就绪,请执行 docker compose logs api postgres redis migrate 查看原因。'
|
||||
throw 'API did not become ready in time. Run docker compose logs api postgres redis migrate for details.'
|
||||
|
||||
@@ -10,7 +10,7 @@ $deviceSecret=[Text.Encoding]::UTF8.GetBytes('local-simulated-device-secret-32-b
|
||||
$configIds=@();$resourceIds=@();$conversationId=$null;$groupId=$null
|
||||
$key1='ops-config-'+[Guid]::NewGuid().ToString('N');$key2='ops-config-'+[Guid]::NewGuid().ToString('N');$rollbackKey='ops-rollback-'+[Guid]::NewGuid().ToString('N')
|
||||
|
||||
function Token([string]$name){(Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType 'application/x-www-form-urlencoded' -Body @{grant_type='password';client_id='voice-platform-web';username=$name;password='LocalOnly123!';scope='openid'}).access_token}
|
||||
function Token([string]$name){$password=if($name-like'operator*'){'OpsLocalOnly123!'}else{'LocalOnly123!'};(Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType 'application/x-www-form-urlencoded' -Body @{grant_type='password';client_id='voice-platform-web';username=$name;password=$password;scope='openid'}).access_token}
|
||||
function Expected([scriptblock]$call,[int]$status){try{&$call|Out-Null;throw "Expected HTTP $status"}catch{$actual=[int]$_.Exception.Response.StatusCode;if($actual-ne$status){throw}}}
|
||||
function SignedPost([string]$path,[string]$body,[string]$key){$bytes=[Text.Encoding]::UTF8.GetBytes($body);$digest=[Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($bytes)).ToLowerInvariant();$timestamp=[DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ');$nonce=[Guid]::NewGuid().ToString('N');$canonical="POST`n$path`n$timestamp`n$nonce`n$digest";$hmac=[Security.Cryptography.HMACSHA256]::new($deviceSecret);try{$signature=[Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonical))).TrimEnd('=').Replace('+','-').Replace('/','_')}finally{$hmac.Dispose()};Invoke-WebRequest -UseBasicParsing -Method Post -Uri ($api+$path) -ContentType 'application/json' -Body $body -Headers @{'X-Device-Id'=$deviceId;'X-Device-Timestamp'=$timestamp;'X-Device-Nonce'=$nonce;'X-Device-Content-SHA256'=$digest;'X-Device-Signature'=$signature;'Idempotency-Key'=$key}}
|
||||
function RolloutBody([string]$reason,[string]$scope,[string]$scopeId=''){@{reason=$reason;rollout=@{scope_type=$scope;rollout_percent=100;max_concurrent_calls=100}}|ForEach-Object{if($scopeId){$_.rollout.scope_id=$scopeId};$_|ConvertTo-Json -Compress -Depth 6}}
|
||||
@@ -18,10 +18,10 @@ function AddEvaluation([string]$configId,[string]$suite,[string]$token){$body=@{
|
||||
function AddResource([string]$type,[string]$name,[string]$display,[string]$token){$body=@{resource_type=$type;reference_name=$name;display_name=$display;verified_at=[DateTime]::UtcNow.ToString('o')}|ConvertTo-Json -Compress;(Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/ops/resource-references" -Headers @{Authorization="Bearer $token";'X-Tenant-Id'=$tenantPublicId;'Idempotency-Key'=('ops-resource-'+[Guid]::NewGuid().ToString('N'))} -ContentType 'application/json' -Body $body).Content|ConvertFrom-Json}
|
||||
|
||||
try{
|
||||
$owner=Token 'owner';$editor=Token 'editor';$outsider=Token 'outsider'
|
||||
$owner=Token 'operator';$editor=Token 'operator-approver';$outsider=Token 'outsider'
|
||||
$ownerMe=((Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/me" -Headers @{Authorization="Bearer $owner"}).Content|ConvertFrom-Json).data
|
||||
$editorMe=((Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/me" -Headers @{Authorization="Bearer $editor"}).Content|ConvertFrom-Json).data
|
||||
if($ownerMe.accounts[0].platform_role-ne'ops_admin'-or$editorMe.accounts[0].platform_role-ne'ops_approver'){throw 'Seeded operations roles were not exposed by /me'}
|
||||
if($ownerMe.accounts[0].account_kind-ne'operator'-or$ownerMe.accounts[0].platform_role-ne'ops_admin'-or$editorMe.accounts[0].account_kind-ne'operator'-or$editorMe.accounts[0].platform_role-ne'ops_approver'){throw 'Dedicated operations accounts were not exposed by /me'}
|
||||
Expected {Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/ops/config-versions" -Headers @{Authorization="Bearer $outsider";'X-Tenant-Id'='20000000-0000-0000-0000-000000000010'}} 403
|
||||
|
||||
$groupBody=@{name=('本地灰度设备组-'+[Guid]::NewGuid().ToString('N').Substring(0,8));selector=@{source='smoke-test'};device_ids=@($deviceId)}|ConvertTo-Json -Compress -Depth 5
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
param(
|
||||
[string[]]$Packages = @(
|
||||
'./internal/deviceauth',
|
||||
'./internal/saas',
|
||||
'./internal/orchestration',
|
||||
'./internal/extraction',
|
||||
'./internal/privacy',
|
||||
'./internal/media',
|
||||
'./internal/retrieval'
|
||||
),
|
||||
[switch]$KeepDatabase
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$PSNativeCommandUseErrorActionPreference = $true
|
||||
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $projectRoot
|
||||
|
||||
$databaseName = 'voice_platform_test_' + ([guid]::NewGuid().ToString('N'))
|
||||
if ($databaseName -notmatch '^voice_platform_test_[0-9a-f]{32}$') {
|
||||
throw "Refusing to use unexpected integration database name: $databaseName"
|
||||
}
|
||||
|
||||
function Invoke-PostgresAdmin {
|
||||
param([Parameter(Mandatory = $true)][string]$Sql)
|
||||
docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U voice -d postgres -c $Sql
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "PostgreSQL administration command failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$postgresID = docker compose ps -q postgres
|
||||
if (-not $postgresID) {
|
||||
Write-Host 'Starting the local PostgreSQL service...'
|
||||
docker compose up -d --wait postgres
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Starting PostgreSQL failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Creating isolated integration database $databaseName"
|
||||
Invoke-PostgresAdmin -Sql "CREATE DATABASE `"$databaseName`" OWNER voice;"
|
||||
|
||||
$containerDatabaseURL = "postgres://voice:voice_dev_only@postgres:5432/$databaseName`?sslmode=disable"
|
||||
docker compose run --rm --no-deps migrate `
|
||||
-path=/migrations `
|
||||
"-database=$containerDatabaseURL" `
|
||||
up
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Database migration failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
docker compose run --rm --no-deps `
|
||||
-e PGPASSWORD=voice_dev_only `
|
||||
seed psql -h postgres -U voice -d $databaseName -v ON_ERROR_STOP=1 -f /fixtures/local_seed.sql
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Database seed failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
$previousTestDatabaseURL = $env:TEST_DATABASE_URL
|
||||
$env:TEST_DATABASE_URL = "postgres://voice:voice_dev_only@127.0.0.1:55432/$databaseName`?sslmode=disable"
|
||||
try {
|
||||
Write-Host "Running database-backed packages against $databaseName"
|
||||
go test -count=1 -p 1 @Packages
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Database integration tests failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
} finally {
|
||||
if ($null -eq $previousTestDatabaseURL) {
|
||||
Remove-Item Env:TEST_DATABASE_URL -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
$env:TEST_DATABASE_URL = $previousTestDatabaseURL
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if ($KeepDatabase) {
|
||||
Write-Warning "Keeping isolated integration database for inspection: $databaseName"
|
||||
} elseif ($databaseName -match '^voice_platform_test_[0-9a-f]{32}$') {
|
||||
Write-Host "Dropping isolated integration database $databaseName"
|
||||
Invoke-PostgresAdmin -Sql "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='$databaseName' AND pid <> pg_backend_pid();"
|
||||
Invoke-PostgresAdmin -Sql "DROP DATABASE IF EXISTS `"$databaseName`";"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user