Files
sub2api/backend/internal/service/update_service_test.go
T
shaw 1c2e6503c6 feat: 版本徽章新增近3个历史版本在线回退与手动回退指引
后端:
- 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 个新增用例
2026-07-09 22:30:04 +08:00

188 lines
5.6 KiB
Go

//go:build unit
package service
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
)
type updateServiceCacheStub struct {
data string
}
func (s *updateServiceCacheStub) GetUpdateInfo(context.Context) (string, error) {
if s.data == "" {
return "", errors.New("cache miss")
}
return s.data, nil
}
func (s *updateServiceCacheStub) SetUpdateInfo(_ context.Context, data string, _ time.Duration) error {
s.data = data
return nil
}
type updateServiceGitHubClientStub struct {
release *GitHubRelease
recentReleases []*GitHubRelease
recentErr error
}
func (s *updateServiceGitHubClientStub) FetchLatestRelease(context.Context, string) (*GitHubRelease, error) {
return s.release, nil
}
func (s *updateServiceGitHubClientStub) FetchRecentReleases(context.Context, string, int) ([]*GitHubRelease, error) {
return s.recentReleases, s.recentErr
}
func (s *updateServiceGitHubClientStub) DownloadFile(context.Context, string, string, int64) error {
panic("DownloadFile should not be called when no update is available")
}
func (s *updateServiceGitHubClientStub) FetchChecksumFile(context.Context, string) ([]byte, error) {
panic("FetchChecksumFile should not be called when no update is available")
}
func TestUpdateServicePerformUpdateNoUpdateReturnsSentinel(t *testing.T) {
svc := NewUpdateService(
&updateServiceCacheStub{},
&updateServiceGitHubClientStub{
release: &GitHubRelease{
TagName: "v0.1.132",
Name: "v0.1.132",
},
},
"0.1.132",
"release",
)
err := svc.PerformUpdate(context.Background())
require.Error(t, err)
require.True(t, errors.Is(err, ErrNoUpdateAvailable))
require.ErrorIs(t, err, ErrNoUpdateAvailable)
}
func newRollbackTestService(current string, releases []*GitHubRelease) *UpdateService {
return NewUpdateService(
&updateServiceCacheStub{},
&updateServiceGitHubClientStub{recentReleases: releases},
current,
"release",
)
}
func TestUpdateServiceListRollbackVersionsFiltersAndCaps(t *testing.T) {
releases := []*GitHubRelease{
{TagName: "v0.1.148", PublishedAt: "2026-07-09T00:00:00Z"}, // newer than current: excluded
{TagName: "v0.1.147", PublishedAt: "2026-07-08T00:00:00Z"}, // current: excluded
{TagName: "v0.1.146-rc1", PublishedAt: "2026-07-07T12:00:00Z", Prerelease: true}, // prerelease: excluded
{TagName: "v0.1.146", PublishedAt: "2026-07-07T00:00:00Z"},
{TagName: "v0.1.145", PublishedAt: "2026-07-06T00:00:00Z", Draft: true}, // draft: excluded
{TagName: "v0.1.144", PublishedAt: "2026-07-05T00:00:00Z"},
{TagName: "v0.1.144", PublishedAt: "2026-07-05T00:00:00Z"}, // duplicate: excluded
{TagName: "v0.1.143", PublishedAt: "2026-07-04T00:00:00Z"},
{TagName: "v0.1.142", PublishedAt: "2026-07-03T00:00:00Z"}, // beyond cap of 3: excluded
}
svc := newRollbackTestService("0.1.147", releases)
versions, err := svc.ListRollbackVersions(context.Background())
require.NoError(t, err)
require.Len(t, versions, 3)
require.Equal(t, "0.1.146", versions[0].Version)
require.Equal(t, "0.1.144", versions[1].Version)
require.Equal(t, "0.1.143", versions[2].Version)
}
func TestUpdateServiceListRollbackVersionsSortsUnorderedInput(t *testing.T) {
releases := []*GitHubRelease{
{TagName: "v0.1.144"},
{TagName: "v0.1.146"},
{TagName: "v0.1.145"},
}
svc := newRollbackTestService("0.1.147", releases)
versions, err := svc.ListRollbackVersions(context.Background())
require.NoError(t, err)
require.Len(t, versions, 3)
require.Equal(t, "0.1.146", versions[0].Version)
require.Equal(t, "0.1.145", versions[1].Version)
require.Equal(t, "0.1.144", versions[2].Version)
}
func TestUpdateServiceListRollbackVersionsEmptyWhenNoneOlder(t *testing.T) {
releases := []*GitHubRelease{
{TagName: "v0.1.147"},
{TagName: "v0.1.148"},
}
svc := newRollbackTestService("0.1.147", releases)
versions, err := svc.ListRollbackVersions(context.Background())
require.NoError(t, err)
require.Empty(t, versions)
}
func TestUpdateServiceListRollbackVersionsPropagatesFetchError(t *testing.T) {
svc := NewUpdateService(
&updateServiceCacheStub{},
&updateServiceGitHubClientStub{recentErr: errors.New("github unavailable")},
"0.1.147",
"release",
)
_, err := svc.ListRollbackVersions(context.Background())
require.Error(t, err)
require.Contains(t, err.Error(), "github unavailable")
}
func TestUpdateServiceRollbackToVersionRejectsDisallowedTargets(t *testing.T) {
releases := []*GitHubRelease{
{TagName: "v0.1.148"},
{TagName: "v0.1.147"},
{TagName: "v0.1.146"},
{TagName: "v0.1.145"},
{TagName: "v0.1.144"},
{TagName: "v0.1.143"},
{TagName: "v0.1.142"},
}
svc := newRollbackTestService("0.1.147", releases)
for _, target := range []string{
"", // empty
"0.1.147", // current version
"v0.1.147", // current version with prefix
"0.1.148", // newer than current
"0.1.142", // older than the 3 most recent
"9.9.9", // nonexistent
} {
err := svc.RollbackToVersion(context.Background(), target)
require.ErrorIs(t, err, ErrRollbackVersionNotAllowed, "target %q should be rejected", target)
}
}
func TestUpdateServiceRollbackToVersionAcceptsVPrefix(t *testing.T) {
// No platform asset in the release: the target passes the allowlist check
// and fails later at asset lookup, proving the version itself was accepted.
releases := []*GitHubRelease{
{TagName: "v0.1.147"},
{TagName: "v0.1.146"},
}
svc := newRollbackTestService("0.1.147", releases)
err := svc.RollbackToVersion(context.Background(), "v0.1.146")
require.Error(t, err)
require.NotErrorIs(t, err, ErrRollbackVersionNotAllowed)
require.Contains(t, err.Error(), "no compatible release found")
}