feat: add trusted mirror update source
Version checks and downloads previously depended on GitHub Release. About & Updates now offers GitHub or a validated mirror source, keeps the selector beneath the startup-check switch, and disables it when startup checks are off. The website resolves platform downloads from a bounded OSS release index with a GitHub fallback. Tagged releases publish and verify immutable OSS assets through OIDC before switching the latest-version index; deployment requires the configured Alibaba Cloud environment variables and role. Release note: “关于与更新”新增 GitHub 与镜像节点选择,启动检查、手动检查和下载页使用同一可信来源;官网下载也可按系统、架构和安装包类型直接选择。
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
checkMirrorForUpdates,
|
||||
checkForUpdates,
|
||||
compareStrictSemVer,
|
||||
GOODBUDDY_LATEST_RELEASE_API_URL
|
||||
getUpdateDownloadPage,
|
||||
GOODBUDDY_LATEST_RELEASE_API_URL,
|
||||
GOODBUDDY_MIRROR_RELEASE_INDEX_URL,
|
||||
VersionChecker
|
||||
} from './version-checker'
|
||||
|
||||
const latestVersion = '1.2.3'
|
||||
@@ -95,6 +99,93 @@ function successfulFetch(): ReturnType<typeof vi.fn<typeof fetch>> {
|
||||
})
|
||||
}
|
||||
|
||||
type MirrorTestFile = {
|
||||
name: string
|
||||
size: number
|
||||
sha256: string
|
||||
url: string
|
||||
}
|
||||
|
||||
type MirrorTestTarget = {
|
||||
platform: 'windows' | 'macos' | 'linux'
|
||||
arch: 'x64' | 'arm64'
|
||||
files: Record<string, MirrorTestFile>
|
||||
}
|
||||
|
||||
type MirrorTestIndex = {
|
||||
formatVersion: 1
|
||||
productName: 'GoodBuddy'
|
||||
version: string
|
||||
targets: Record<string, MirrorTestTarget>
|
||||
checksumUrl: string
|
||||
fallbackUrl: string
|
||||
}
|
||||
|
||||
function mirrorFileName(
|
||||
platform: MirrorTestTarget['platform'],
|
||||
arch: MirrorTestTarget['arch'],
|
||||
format: string
|
||||
): string {
|
||||
const suffixes: Record<string, string> = {
|
||||
nsis: 'setup.exe',
|
||||
portable: 'portable.zip',
|
||||
dmg: 'installer.dmg',
|
||||
zip: 'portable.zip',
|
||||
AppImage: 'portable.AppImage',
|
||||
deb: 'installer.deb'
|
||||
}
|
||||
return `GoodBuddy-${latestVersion}-${platform}-${arch}-${suffixes[format]}`
|
||||
}
|
||||
|
||||
function mirrorIndexPayload(): MirrorTestIndex {
|
||||
const definitions: Array<{
|
||||
platform: MirrorTestTarget['platform']
|
||||
arch: MirrorTestTarget['arch']
|
||||
formats: string[]
|
||||
}> = [
|
||||
{ platform: 'windows', arch: 'x64', formats: ['nsis', 'portable'] },
|
||||
{ platform: 'windows', arch: 'arm64', formats: ['nsis', 'portable'] },
|
||||
{ platform: 'macos', arch: 'x64', formats: ['dmg', 'zip'] },
|
||||
{ platform: 'macos', arch: 'arm64', formats: ['dmg', 'zip'] },
|
||||
{ platform: 'linux', arch: 'x64', formats: ['AppImage', 'deb'] },
|
||||
{ platform: 'linux', arch: 'arm64', formats: ['AppImage', 'deb'] }
|
||||
]
|
||||
const releaseBase =
|
||||
`https://goodbuddy.oss-cn-hangzhou.aliyuncs.com/releases/` +
|
||||
`v${latestVersion}/`
|
||||
const targets: Record<string, MirrorTestTarget> = {}
|
||||
for (const definition of definitions) {
|
||||
const targetFiles: Record<string, MirrorTestFile> = {}
|
||||
for (const [index, format] of definition.formats.entries()) {
|
||||
const name = mirrorFileName(
|
||||
definition.platform,
|
||||
definition.arch,
|
||||
format
|
||||
)
|
||||
targetFiles[format] = {
|
||||
name,
|
||||
size: 100 + index,
|
||||
sha256: (index === 0 ? 'a' : 'b').repeat(64),
|
||||
url: new URL(encodeURIComponent(name), releaseBase).href
|
||||
}
|
||||
}
|
||||
targets[`${definition.platform}-${definition.arch}`] = {
|
||||
platform: definition.platform,
|
||||
arch: definition.arch,
|
||||
files: targetFiles
|
||||
}
|
||||
}
|
||||
return {
|
||||
formatVersion: 1,
|
||||
productName: 'GoodBuddy',
|
||||
version: latestVersion,
|
||||
targets,
|
||||
checksumUrl: new URL('SHA256SUMS', releaseBase).href,
|
||||
fallbackUrl:
|
||||
'https://github.com/mesalogo/goodbuddy/releases/latest'
|
||||
}
|
||||
}
|
||||
|
||||
describe('compareStrictSemVer', () => {
|
||||
it('implements SemVer precedence without treating build metadata as newer', () => {
|
||||
expect(compareStrictSemVer('1.0.0-alpha.2', '1.0.0-alpha.10')).toBe(-1)
|
||||
@@ -474,3 +565,111 @@ describe('checkForUpdates', () => {
|
||||
).rejects.toMatchObject({ name: 'AbortError' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkMirrorForUpdates', () => {
|
||||
it('reads the fixed mirror index and returns the current platform files', async () => {
|
||||
const payload = mirrorIndexPayload()
|
||||
const transport = vi.fn<typeof fetch>(async () =>
|
||||
jsonResponse(payload)
|
||||
)
|
||||
|
||||
await expect(
|
||||
checkMirrorForUpdates({
|
||||
fetch: transport,
|
||||
currentVersion: '1.0.0',
|
||||
platform: 'win32',
|
||||
arch: 'x64'
|
||||
})
|
||||
).resolves.toEqual({
|
||||
updateAvailable: true,
|
||||
currentVersion: '1.0.0',
|
||||
latestVersion,
|
||||
releaseUrl: 'https://mesalogo.github.io/goodbuddy/#download',
|
||||
target: {
|
||||
platform: 'windows',
|
||||
arch: 'x64',
|
||||
formats: ['nsis', 'portable'],
|
||||
files: Object.values(
|
||||
payload.targets['windows-x64']!.files
|
||||
).map((file) => ({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
sha256: file.sha256
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
expect(transport).toHaveBeenCalledTimes(1)
|
||||
expect(String(transport.mock.calls[0]?.[0])).toBe(
|
||||
GOODBUDDY_MIRROR_RELEASE_INDEX_URL
|
||||
)
|
||||
expect(transport.mock.calls[0]?.[1]).toMatchObject({
|
||||
method: 'GET',
|
||||
redirect: 'manual',
|
||||
credentials: 'omit',
|
||||
cache: 'no-store'
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects redirects, incomplete targets, and untrusted file URLs', async () => {
|
||||
const redirecting = vi.fn<typeof fetch>(async () =>
|
||||
new Response(null, {
|
||||
status: 302,
|
||||
headers: { location: 'https://attacker.invalid/latest.json' }
|
||||
})
|
||||
)
|
||||
await expect(
|
||||
checkMirrorForUpdates({
|
||||
fetch: redirecting,
|
||||
currentVersion: '1.0.0',
|
||||
platform: 'win32',
|
||||
arch: 'x64'
|
||||
})
|
||||
).rejects.toThrow('must not redirect')
|
||||
|
||||
const incomplete = mirrorIndexPayload()
|
||||
delete incomplete.targets['linux-arm64']
|
||||
await expect(
|
||||
checkMirrorForUpdates({
|
||||
fetch: vi.fn<typeof fetch>(async () => jsonResponse(incomplete)),
|
||||
currentVersion: '1.0.0',
|
||||
platform: 'win32',
|
||||
arch: 'x64'
|
||||
})
|
||||
).rejects.toThrow('targets are incomplete')
|
||||
|
||||
const untrusted = mirrorIndexPayload()
|
||||
untrusted.targets['windows-x64']!.files.nsis!.url =
|
||||
'https://attacker.invalid/GoodBuddy.exe'
|
||||
await expect(
|
||||
checkMirrorForUpdates({
|
||||
fetch: vi.fn<typeof fetch>(async () => jsonResponse(untrusted)),
|
||||
currentVersion: '1.0.0',
|
||||
platform: 'win32',
|
||||
arch: 'x64'
|
||||
})
|
||||
).rejects.toThrow('not a trusted mirror URL')
|
||||
})
|
||||
|
||||
it('routes VersionChecker and download pages through the selected source', async () => {
|
||||
const transport = vi.fn<typeof fetch>(async () =>
|
||||
jsonResponse(mirrorIndexPayload())
|
||||
)
|
||||
const checker = new VersionChecker({
|
||||
fetch: transport,
|
||||
currentVersion: '1.0.0',
|
||||
platform: 'win32',
|
||||
arch: 'x64'
|
||||
})
|
||||
|
||||
await expect(checker.check('mirror')).resolves.toMatchObject({
|
||||
latestVersion
|
||||
})
|
||||
expect(getUpdateDownloadPage('github')).toBe(
|
||||
'https://github.com/mesalogo/goodbuddy/releases'
|
||||
)
|
||||
expect(getUpdateDownloadPage('mirror')).toBe(
|
||||
'https://mesalogo.github.io/goodbuddy/#download'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user