后端:
- UpdateService 新增 ListRollbackVersions/RollbackToVersion,回退目标强制
限定为比当前版本更旧的近3个正式版本(排除当前/更新/预发布/草稿),
越界返回 400 ROLLBACK_VERSION_NOT_ALLOWED;下载复用既有 HTTPS 域名
白名单 + checksum 校验 + 原子换二进制管线
- GitHubReleaseClient 新增 FetchRecentReleases(releases 列表 API)
- 新增 GET /admin/system/rollback-versions;POST /admin/system/rollback
支持可选 {"version"} body,无 body 保持原 .backup 本地回退行为不变
- 端点均在 /admin 组 adminAuth(IsAdmin+TokenVersion)保护内
前端:
- VersionBadge「已是最新版本」状态下新增版本回退面板:radio 卡片选择
近3个版本,手动回退方式按部署形态 tab 切换(脚本部署 curl 命令 /
Docker 镜像 tag 指引),支持一键复制;release 构建提供一键回退并
复用重启流程;源码构建仅提示不支持在线回退
- 下拉根元素重置 whitespace-normal,修复 sidebar-brand 的
white-space:nowrap 继承导致的长文本溢出裁切
测试:服务层过滤/排序/上限/非法目标拒绝、handler 双模式与鉴权错误映射、
GitHub client 列表拉取、前端 API 请求体行为共 16 个新增用例
56 lines
1.5 KiB
TypeScript
56 lines
1.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { get, post } = vi.hoisted(() => ({
|
|
get: vi.fn(),
|
|
post: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../client', () => ({
|
|
apiClient: {
|
|
get,
|
|
post,
|
|
},
|
|
}))
|
|
|
|
import { getRollbackVersions, rollback, type RollbackVersionInfo } from '@/api/admin/system'
|
|
|
|
describe('admin system rollback API', () => {
|
|
beforeEach(() => {
|
|
get.mockReset()
|
|
post.mockReset()
|
|
})
|
|
|
|
it('getRollbackVersions fetches the rollback version list', async () => {
|
|
const versions: RollbackVersionInfo[] = [
|
|
{
|
|
version: '0.1.146',
|
|
published_at: '2026-07-07T00:00:00Z',
|
|
html_url: 'https://github.com/Wei-Shaw/sub2api/releases/tag/v0.1.146'
|
|
}
|
|
]
|
|
get.mockResolvedValue({ data: { versions } })
|
|
|
|
const result = await getRollbackVersions()
|
|
|
|
expect(get).toHaveBeenCalledWith('/admin/system/rollback-versions')
|
|
expect(result.versions).toEqual(versions)
|
|
})
|
|
|
|
it('rollback posts the target version in the request body', async () => {
|
|
post.mockResolvedValue({ data: { message: 'ok', need_restart: true } })
|
|
|
|
const result = await rollback('0.1.146')
|
|
|
|
expect(post).toHaveBeenCalledWith('/admin/system/rollback', { version: '0.1.146' })
|
|
expect(result.need_restart).toBe(true)
|
|
})
|
|
|
|
it('rollback without a version posts no body (legacy backup rollback)', async () => {
|
|
post.mockResolvedValue({ data: { message: 'ok', need_restart: true } })
|
|
|
|
await rollback()
|
|
|
|
expect(post).toHaveBeenCalledWith('/admin/system/rollback', undefined)
|
|
})
|
|
})
|