2026-09-29 20:17:36 +08:00
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
|
import { spawnSync } from 'node:child_process'
|
|
|
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
|
|
|
|
|
const publisher = fileURLToPath(new URL('./publish-nacos-config.ps1', import.meta.url))
|
|
|
|
|
const source = readFileSync(publisher, 'utf8')
|
|
|
|
|
const quote = value => `'${value.replaceAll("'", "''")}'`
|
|
|
|
|
|
|
|
|
|
function run(mode) {
|
|
|
|
|
// Override the HTTP cmdlet before invoking the actual publisher: no network is possible.
|
|
|
|
|
const command = `
|
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
|
$global:mode = ${quote(mode)}
|
|
|
|
|
$global:calls = 0
|
|
|
|
|
$global:published = 0
|
|
|
|
|
function Invoke-RestMethod {
|
|
|
|
|
param($Method, $Uri, $Body, $TimeoutSec, $ContentType)
|
|
|
|
|
$global:calls++
|
|
|
|
|
if ($global:mode -eq 'dry') { throw 'DryRun attempted HTTP' }
|
|
|
|
|
if ($Uri -eq 'http://offline.invalid/nacos/v3/auth/user/login') {
|
|
|
|
|
if ($Method -ne 'Post' -or $Body.username -ne 'fixture-user' -or $Body.password -ne 'fixture-password') { throw 'Bad login request' }
|
|
|
|
|
if ($global:mode -eq 'login-failure') { return @{} }
|
|
|
|
|
return @{accessToken='fixture-token'}
|
|
|
|
|
}
|
|
|
|
|
if ($Uri -eq 'http://offline.invalid/nacos/v3/admin/core/namespace/list') {
|
|
|
|
|
if ($Method -ne 'Get' -or $Body.accessToken -ne 'fixture-token') { throw 'Bad namespace request' }
|
|
|
|
|
if ($global:mode -eq 'missing-namespace') { return @{code=0;data=@(@{namespace='other';namespaceShowName='fixture-id'})} }
|
|
|
|
|
if ($global:mode -eq 'namespace-failure') { return @{code=500;data=@(@{namespace='fixture-id'})} }
|
|
|
|
|
return @{code=0;data=@(@{namespace='fixture-id';namespaceShowName='TEST'})}
|
|
|
|
|
}
|
|
|
|
|
if ($Uri -ne 'http://offline.invalid/nacos/v3/admin/cs/config' -or $Method -ne 'Post') { throw 'Unexpected endpoint' }
|
|
|
|
|
$form = @{}
|
|
|
|
|
foreach ($pair in $Body.Split('&')) {
|
|
|
|
|
$parts = $pair.Split('=',2)
|
|
|
|
|
$form[[uri]::UnescapeDataString($parts[0])] = [uri]::UnescapeDataString($parts[1])
|
|
|
|
|
}
|
|
|
|
|
if ($ContentType -ne 'application/x-www-form-urlencoded; charset=UTF-8' -or
|
|
|
|
|
$form.namespaceId -ne 'fixture-id' -or $form.groupName -ne 'BAIYUE' -or
|
|
|
|
|
$form.accessToken -ne 'fixture-token' -or $form.type -ne 'yaml' -or
|
|
|
|
|
$form.ContainsKey('tenant') -or $form.ContainsKey('group')) { throw 'Bad config form' }
|
|
|
|
|
$expected = Get-Content -LiteralPath (Join-Path ${quote(fileURLToPath(new URL('../config/nacos/test/', import.meta.url)))} $form.dataId) -Raw -Encoding UTF8
|
|
|
|
|
if ($form.content -cne $expected) { throw 'YAML changed during encoding' }
|
|
|
|
|
if ($global:published -eq 0 -and $form.dataId -ne 'baiyue-common.yml') { throw 'Common config must publish first' }
|
|
|
|
|
$global:published++
|
|
|
|
|
switch ($global:mode) {
|
|
|
|
|
'false-data' { return @{code=0;data=$false} }
|
|
|
|
|
'error-code' { return @{code=500;data=$true} }
|
|
|
|
|
'missing-code' { return @{data=$true} }
|
|
|
|
|
'string-data' { return @{code=0;data='true'} }
|
|
|
|
|
'legacy-response' { return $true }
|
|
|
|
|
default { return @{code=0;data=$true} }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$failed = $false
|
|
|
|
|
try {
|
|
|
|
|
& ${quote(publisher)} -ServerAddr offline.invalid -Namespace fixture-id -Username fixture-user -Password fixture-password -DryRun:($global:mode -eq 'dry')
|
|
|
|
|
} catch { $failed = $true }
|
|
|
|
|
[ordered]@{failed=$failed; calls=$global:calls; published=$global:published} | ConvertTo-Json -Compress
|
|
|
|
|
`
|
2026-10-21 15:10:44 +08:00
|
|
|
// 使用项目统一的 PowerShell 7,避免子进程回落 Windows PowerShell 5 的模块路径与解析环境。
|
|
|
|
|
const result = spawnSync('pwsh',
|
2026-09-29 20:17:36 +08:00
|
|
|
['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(command, 'utf16le').toString('base64')],
|
|
|
|
|
{ encoding: 'utf8', timeout: 30_000 })
|
|
|
|
|
assert.equal(result.status, 0, result.stderr || result.error?.message)
|
|
|
|
|
return JSON.parse(result.stdout.trim().split(/\r?\n/).at(-1))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('publisher uses v3 only and keeps TEST namespace validation', () => {
|
|
|
|
|
assert.doesNotMatch(source, /\/v1\/|\/v2\/|tenant=|\bgroup=/)
|
|
|
|
|
assert.match(source, /ValidateSet\('test'\)/)
|
|
|
|
|
assert.match(source, /Namespace -eq 'public'/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('DryRun makes zero HTTP calls without publishing', () => {
|
|
|
|
|
assert.deepEqual(run('dry'), { failed: false, calls: 0, published: 0 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('v3 publishes all 14 original YAML documents with common first', () => {
|
|
|
|
|
assert.deepEqual(run('success'), { failed: false, calls: 16, published: 14 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
for (const mode of ['login-failure', 'missing-namespace', 'namespace-failure']) {
|
|
|
|
|
test(`${mode} prevents all configuration writes`, () => {
|
|
|
|
|
assert.deepEqual(run(mode), { failed: true, calls: mode === 'login-failure' ? 1 : 2, published: 0 })
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const mode of ['false-data', 'error-code', 'missing-code', 'string-data', 'legacy-response']) {
|
|
|
|
|
test(`${mode} stops at the first failed publish`, () => {
|
|
|
|
|
assert.deepEqual(run(mode), { failed: true, calls: 3, published: 1 })
|
|
|
|
|
})
|
|
|
|
|
}
|