desktop/electron/serverConfig.test.cjs
Rama main · réplica de lectura del repositorio original.
'use strict'
const test = require('node:test')
const assert = require('node:assert/strict')
const fs = require('fs')
const os = require('os')
const path = require('path')
const {
DEFAULT_SERVER_BASE_URL,
HISTORY_LIMIT,
normalizeServerBaseUrl,
deriveApiBase,
deriveUpdateFeedUrl,
serverHostKey,
readServerConfig,
writeServerConfig,
resolveUpdateFeedUrl,
probeServer,
defaultServerConfig,
getServerHistoryPath,
readServerHistory,
recordServerHistory,
removeServerHistory,
} = require('./serverConfig.cjs')
function tempUserData(t) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mao-server-config-'))
t.after(() => fs.rmSync(dir, { recursive: true, force: true }))
return dir
}
test('normalizeServerBaseUrl accepts site root, /api and /api/v1', () => {
const cases = [
['https://mao.example.com', 'https://mao.example.com'],
['https://mao.example.com/', 'https://mao.example.com'],
['https://mao.example.com/api', 'https://mao.example.com'],
['https://mao.example.com/api/v1', 'https://mao.example.com'],
['https://mao.example.com/api/v1/', 'https://mao.example.com'],
['mao.example.com', 'https://mao.example.com'],
['https://mao.example.com:8443/api/v1', 'https://mao.example.com:8443'],
]
for (const [input, expected] of cases) {
const result = normalizeServerBaseUrl(input, { allowHttp: true })
assert.equal(result.ok, true, input)
assert.equal(result.baseUrl, expected, input)
}
})
test('normalizeServerBaseUrl rejects invalid or insecure input by default', () => {
assert.equal(normalizeServerBaseUrl('').ok, false)
assert.equal(normalizeServerBaseUrl('http://mao.example.com').ok, false)
assert.equal(normalizeServerBaseUrl('ftp://mao.example.com').ok, false)
assert.equal(normalizeServerBaseUrl('https://user:pass@mao.example.com').ok, false)
const httpAllowed = normalizeServerBaseUrl('http://10.0.0.8:9080', { allowHttp: true })
assert.equal(httpAllowed.ok, true)
assert.equal(httpAllowed.baseUrl, 'http://10.0.0.8:9080')
})
test('derive helpers and host key', () => {
assert.equal(deriveApiBase('https://mao.example.com'), 'https://mao.example.com/api')
assert.equal(
deriveUpdateFeedUrl('https://mao.example.com'),
'https://mao.example.com/api/uploads/releases/'
)
assert.equal(serverHostKey('https://mao.example.com'), 'mao.example.com')
assert.equal(serverHostKey('https://10.0.0.8:9080'), '10.0.0.8_9080')
})
test('readServerConfig returns defaults when file missing', (t) => {
const dir = tempUserData(t)
const { config, fromFile } = readServerConfig(dir)
assert.equal(fromFile, false)
assert.equal(config.serverBaseUrl, DEFAULT_SERVER_BASE_URL)
assert.equal(config.configured, false)
assert.equal(config.updateFeedMode, 'follow-site')
})
test('writeServerConfig persists normalized baseUrl', (t) => {
const dir = tempUserData(t)
const result = writeServerConfig(dir, {
serverBaseUrl: 'https://mao.example.com/api/v1',
updateFeedMode: 'follow-site',
})
assert.equal(result.ok, true)
assert.equal(result.config.serverBaseUrl, 'https://mao.example.com')
const reread = readServerConfig(dir)
assert.equal(reread.fromFile, true)
assert.equal(reread.config.serverBaseUrl, 'https://mao.example.com')
assert.equal(reread.config.configured, true)
})
test('writeServerConfig rejects invalid url', (t) => {
const dir = tempUserData(t)
const result = writeServerConfig(dir, { serverBaseUrl: 'ftp://bad' })
assert.equal(result.ok, false)
assert.match(result.error, /协议|无效|地址/)
})
test('resolveUpdateFeedUrl modes', () => {
const base = { serverBaseUrl: 'https://mao.example.com', updateFeedMode: 'follow-site' }
assert.deepEqual(resolveUpdateFeedUrl(base, ''), {
mode: 'follow-site',
url: 'https://mao.example.com/api/uploads/releases/',
})
assert.deepEqual(resolveUpdateFeedUrl({ ...base, updateFeedMode: 'disabled' }, ''), {
mode: 'disabled',
url: null,
})
assert.deepEqual(resolveUpdateFeedUrl({ ...base, updateFeedMode: 'package-default' }, ''), {
mode: 'package-default',
url: null,
})
assert.deepEqual(resolveUpdateFeedUrl(base, 'https://cdn.example.com/releases/'), {
mode: 'env-override',
url: 'https://cdn.example.com/releases/',
})
})
test('probeServer accepts version.json payload', async () => {
const fetchImpl = async (url) => {
if (String(url).endsWith('/version.json')) {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({ version: '0.0.163', buildTime: 'x' }),
}
}
return { ok: false, status: 404, text: async () => '' }
}
const result = await probeServer('https://mao.example.com', { fetchImpl })
assert.equal(result.ok, true)
assert.match(result.detail, /0\.0\.163/)
})
test('probeServer flags non-Mao root html', async () => {
const fetchImpl = async (url) => {
if (String(url).endsWith('/version.json')) {
return { ok: false, status: 404, text: async () => 'nope' }
}
return {
ok: true,
status: 200,
text: async () => '<html><title>Other App</title></html>',
}
}
const result = await probeServer('https://other.example.com', { fetchImpl })
assert.equal(result.ok, false)
assert.match(result.error, /不是 Mao/)
})
test('server history MRU: record, dedupe, limit and remove', (t) => {
const dir = tempUserData(t)
assert.deepEqual(readServerHistory(dir), [])
recordServerHistory(dir, 'https://a.example.com', false)
recordServerHistory(dir, 'https://b.example.com', false)
let list = readServerHistory(dir)
assert.equal(list.length, 2)
assert.equal(list[0].serverBaseUrl, 'https://b.example.com')
assert.equal(list[1].serverBaseUrl, 'https://a.example.com')
// 再连 a → 置顶去重,不重复
recordServerHistory(dir, 'https://a.example.com/api/v1', false)
list = readServerHistory(dir)
assert.equal(list.length, 2)
assert.equal(list[0].serverBaseUrl, 'https://a.example.com')
assert.ok(list[0].lastConnectedAt)
// 历史只写归一化后的干净地址(无 /api 后缀)
const dirty = recordServerHistory(dir, 'https://c.example.com/api', true)
assert.equal(dirty[0].serverBaseUrl, 'https://c.example.com')
assert.equal(dirty[0].allowHttp, true)
const trimmed = removeServerHistory(dir, 'https://b.example.com')
assert.equal(trimmed.some((e) => e.serverBaseUrl === 'https://b.example.com'), false)
assert.equal(readServerHistory(dir).length, trimmed.length)
})
test('server history is capped at HISTORY_LIMIT', (t) => {
const dir = tempUserData(t)
for (let i = 0; i < 15; i++) {
recordServerHistory(dir, `https://h${i}.example.com`, false)
}
const list = readServerHistory(dir)
assert.equal(list.length, HISTORY_LIMIT)
assert.equal(list[0].serverBaseUrl, 'https://h14.example.com')
assert.equal(list[list.length - 1].serverBaseUrl, 'https://h5.example.com')
})
test('readServerHistory tolerates missing or corrupt file', (t) => {
const dir = tempUserData(t)
const filePath = getServerHistoryPath(dir)
fs.mkdirSync(path.dirname(filePath), { recursive: true })
fs.writeFileSync(filePath, 'not json', 'utf8')
assert.deepEqual(readServerHistory(dir), [])
fs.writeFileSync(filePath, '[]', 'utf8')
assert.deepEqual(readServerHistory(dir), [])
})
test('defaultServerConfig shape', () => {
const config = defaultServerConfig()
assert.equal(config.version, 1)
assert.equal(config.configured, false)
})