Files
MyGoNavi/internal/app/update_manifest_test.go
Syngnat 3beab030f4 feat(update): 检查更新优先静态 latest.json,避免用户撞 GitHub API 限流
- 客户端优先下载 releases/latest/download/latest.json,再回退 REST API
- 成功结果写入磁盘缓存,限流/断网时可 stale 回退
- 静默检查增加节流;限流文案改为用户无需配置 Token
- CI 与 build-release 发版时生成 latest.json 清单
- 补充静态清单/缓存/节流单测与生成脚本测试
2026-07-09 12:35:12 +08:00

189 lines
5.5 KiB
Go

package app
import (
"encoding/json"
"errors"
"os"
"strings"
"sync"
"testing"
"time"
)
func TestReleaseFromUpdateManifestMapsAssets(t *testing.T) {
release := releaseFromUpdateManifest(&updateReleaseManifest{
TagName: "v1.2.3",
Version: "1.2.3",
Name: "GoNavi 1.2.3",
HTMLURL: "https://github.com/Syngnat/GoNavi/releases/tag/v1.2.3",
PublishedAt: "2026-07-09T00:00:00Z",
Assets: []updateManifestAsset{
{
Name: "GoNavi-1.2.3-Windows-Amd64.exe",
URL: "https://example.com/app.exe",
SHA256: "Aa" + strings.Repeat("b", 62),
Size: 99,
},
},
})
if release == nil {
t.Fatal("expected release")
}
if release.TagName != "v1.2.3" || len(release.Assets) != 1 {
t.Fatalf("unexpected release: %#v", release)
}
if release.Assets[0].BrowserDownloadURL != "https://example.com/app.exe" {
t.Fatalf("url = %q", release.Assets[0].BrowserDownloadURL)
}
if !strings.HasPrefix(release.Assets[0].Digest, "sha256:") {
t.Fatalf("digest = %q", release.Assets[0].Digest)
}
}
func TestDiskUpdateManifestRoundTrip(t *testing.T) {
root := t.TempDir()
t.Setenv("GONAVI_DATA_ROOT", root)
payload := &updateReleaseManifest{
SchemaVersion: 1,
Channel: "latest",
TagName: "v9.9.9",
Version: "9.9.9",
Name: "v9.9.9",
HTMLURL: "https://github.com/Syngnat/GoNavi/releases/tag/v9.9.9",
Assets: []updateManifestAsset{
{Name: "app.bin", URL: "https://example.com/app.bin", SHA256: strings.Repeat("c", 64), Size: 1},
},
FetchedAt: time.Now().UTC(),
}
storeDiskUpdateManifest(updateChannelLatest, payload)
loaded, stale := loadDiskUpdateManifest(updateChannelLatest)
if loaded == nil || stale {
t.Fatalf("expected fresh disk cache, got %#v stale=%v", loaded, stale)
}
if loaded.Version != "9.9.9" {
t.Fatalf("version = %q", loaded.Version)
}
path := updateManifestCachePath(updateChannelLatest)
if _, err := os.Stat(path); err != nil {
t.Fatalf("cache file missing: %v", err)
}
if !strings.HasPrefix(path, root) {
t.Fatalf("cache path not under data root: %s", path)
}
}
func TestFetchReleaseForChannelPreferringStaticUsesStaticFirst(t *testing.T) {
root := t.TempDir()
t.Setenv("GONAVI_DATA_ROOT", root)
updateReleaseCache = sync.Map{}
staticCalled := false
apiCalled := false
restoreStatic := swapUpdateFetchStaticManifest(func(channel updateChannel) (*githubRelease, error) {
staticCalled = true
return &githubRelease{TagName: "v2.0.0", Name: "v2.0.0"}, nil
})
defer restoreStatic()
restoreAPI := swapUpdateFetchLatestRelease(func() (*githubRelease, error) {
apiCalled = true
return nil, errors.New("api should not be called")
})
defer restoreAPI()
release, err := fetchReleaseForChannelPreferringStatic(updateChannelLatest, true)
if err != nil {
t.Fatalf("err=%v", err)
}
if !staticCalled || apiCalled {
t.Fatalf("staticCalled=%v apiCalled=%v", staticCalled, apiCalled)
}
if release.TagName != "v2.0.0" {
t.Fatalf("tag=%q", release.TagName)
}
}
func TestFetchReleaseForChannelPreferringStaticFallsBackAPIThenDisk(t *testing.T) {
root := t.TempDir()
t.Setenv("GONAVI_DATA_ROOT", root)
updateReleaseCache = sync.Map{}
storeDiskUpdateManifest(updateChannelLatest, &updateReleaseManifest{
SchemaVersion: 1,
TagName: "v1.0.0",
Version: "1.0.0",
Assets: []updateManifestAsset{{Name: "a", URL: "https://example.com/a"}},
FetchedAt: time.Now().UTC(),
})
restoreStatic := swapUpdateFetchStaticManifest(func(channel updateChannel) (*githubRelease, error) {
return nil, errors.New("static 404")
})
defer restoreStatic()
restoreAPI := swapUpdateFetchLatestRelease(func() (*githubRelease, error) {
return nil, errors.New("api rate limit")
})
defer restoreAPI()
release, err := fetchReleaseForChannelPreferringStatic(updateChannelLatest, true)
if err != nil {
t.Fatalf("expected disk fallback, err=%v", err)
}
if release.TagName != "v1.0.0" {
t.Fatalf("tag=%q", release.TagName)
}
}
func TestSilentCheckThrottleReusesDisk(t *testing.T) {
root := t.TempDir()
t.Setenv("GONAVI_DATA_ROOT", root)
updateReleaseCache = sync.Map{}
storeDiskUpdateManifest(updateChannelLatest, &updateReleaseManifest{
SchemaVersion: 1,
TagName: "v3.0.0",
Version: "3.0.0",
Assets: []updateManifestAsset{{Name: "a", URL: "u"}},
FetchedAt: time.Now().UTC(),
})
markUpdateNetworkCheck(updateChannelLatest)
staticCalls := 0
restoreStatic := swapUpdateFetchStaticManifest(func(channel updateChannel) (*githubRelease, error) {
staticCalls++
return nil, errors.New("should not hit network")
})
defer restoreStatic()
restoreAPI := swapUpdateFetchLatestRelease(func() (*githubRelease, error) {
t.Fatal("api should not be called during throttle")
return nil, nil
})
defer restoreAPI()
release, err := fetchReleaseForChannelPreferringStatic(updateChannelLatest, false)
if err != nil {
t.Fatalf("err=%v", err)
}
if release.TagName != "v3.0.0" {
t.Fatalf("tag=%q", release.TagName)
}
if staticCalls != 0 {
t.Fatalf("staticCalls=%d", staticCalls)
}
}
func TestUpdateManifestFromGitHubRelease(t *testing.T) {
release := &githubRelease{
TagName: "v1.0.1",
Name: "v1.0.1",
HTMLURL: "https://github.com/Syngnat/GoNavi/releases/tag/v1.0.1",
Assets: []githubAsset{
{Name: "a.exe", BrowserDownloadURL: "https://x/a.exe", Digest: "sha256:" + strings.Repeat("d", 64), Size: 10},
},
}
m := updateManifestFromGitHubRelease(updateChannelLatest, release, nil)
if m == nil || m.Version == "" || len(m.Assets) != 1 {
t.Fatalf("manifest=%#v", m)
}
_ = json.Marshal
}