feat(updater): custom Gitea releases provider with fail-closed verification
Wails ships no Gitea provider, so add updater/gitea (~250 lines): latest non-draft release newer than current wins, pre-releases skipped unless AllowPrerelease, exact AniTrack-linux-amd64 asset match, .sha512/.sig sidecars parsed into updater.Verification (sha512+ed25519ph, mirroring wails3 updater sign). Anything missing or malformed is an error, never an unsigned install. Includes semver compare, streaming download with progress, and unit tests (picking rules, fail-closed sidecars, server errors, progress) plus an opt-in live probe (ANITRACK_LIVE_TEST=1) proven against the real instance. Also: embed updater.pub (public half; private key lives in password manager + UPDATER_SIGNING_KEY CI secret, updater.key gitignored) and ignore CI-side updater-dist/ staging.
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
// Package gitea implements updater.Provider for Gitea releases.
|
||||
//
|
||||
// Releases live on a self-hosted Gitea instance, for which Wails ships no
|
||||
// in-tree provider, so this mirrors the in-tree github provider against
|
||||
// Gitea's releases API. Everything else (verification, staging, swap,
|
||||
// restart, window) stays the framework's job.
|
||||
//
|
||||
// Asset layout per release (see .gitea/workflows/release.yml):
|
||||
//
|
||||
// AniTrack-linux-amd64 bare updater binary, exact name match
|
||||
// AniTrack-linux-amd64.sha512 "<hex> <filename>" (sha512sum format)
|
||||
// AniTrack-linux-amd64.sig base64 ed25519ph signature from
|
||||
// `wails3 updater sign`
|
||||
//
|
||||
// A release missing any of the three is skipped: verification fails closed.
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/updater"
|
||||
)
|
||||
|
||||
// Config configures the Gitea provider.
|
||||
type Config struct {
|
||||
// BaseURL is the instance root, e.g. https://git.linuxhg.com.
|
||||
BaseURL string
|
||||
// Owner and Repo identify the repository, e.g. john-okeefe/Anitrack.
|
||||
Owner string
|
||||
Repo string
|
||||
// AssetName is the exact updater artifact name, e.g. AniTrack-linux-amd64.
|
||||
AssetName string
|
||||
// HTTPClient overrides the default client (30s timeout). Optional.
|
||||
HTTPClient *http.Client
|
||||
// AllowPrerelease includes pre-release-tagged releases. Default false:
|
||||
// -rc tags stay invisible (stable channel). Beta/test flows opt in.
|
||||
AllowPrerelease bool
|
||||
}
|
||||
|
||||
// Provider is an updater.Provider backed by Gitea releases.
|
||||
type Provider struct {
|
||||
cfg Config
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// New validates config and returns a Provider.
|
||||
func New(cfg Config) (*Provider, error) {
|
||||
cfg.BaseURL = strings.TrimSuffix(cfg.BaseURL, "/")
|
||||
if cfg.BaseURL == "" || cfg.Owner == "" || cfg.Repo == "" || cfg.AssetName == "" {
|
||||
return nil, fmt.Errorf("gitea: BaseURL, Owner, Repo and AssetName are all required")
|
||||
}
|
||||
client := cfg.HTTPClient
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 30 * time.Second}
|
||||
}
|
||||
return &Provider{cfg: cfg, client: client}, nil
|
||||
}
|
||||
|
||||
// Name identifies the provider in logs and event payloads.
|
||||
func (p *Provider) Name() string { return "gitea" }
|
||||
|
||||
// giteaRelease mirrors the fields we read from the Gitea releases API.
|
||||
type giteaRelease struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
Draft bool `json:"draft"`
|
||||
Prerelease bool `json:"prerelease"`
|
||||
PublishedAt string `json:"published_at"`
|
||||
Assets []giteaAsset `json:"assets"`
|
||||
}
|
||||
|
||||
// giteaAsset mirrors one release attachment.
|
||||
type giteaAsset struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
// Check finds the newest usable release newer than req.CurrentVersion.
|
||||
// (nil, nil) means up-to-date.
|
||||
func (p *Provider) Check(ctx context.Context, req updater.CheckRequest) (*updater.Release, error) {
|
||||
releases, err := p.listReleases(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var best *giteaRelease
|
||||
var bestAsset *giteaAsset
|
||||
for i := range releases {
|
||||
r := &releases[i]
|
||||
if r.Draft {
|
||||
continue
|
||||
}
|
||||
if r.Prerelease && !p.cfg.AllowPrerelease {
|
||||
continue
|
||||
}
|
||||
if compareSemver(r.TagName, req.CurrentVersion) <= 0 {
|
||||
continue
|
||||
}
|
||||
asset := findAsset(r.Assets, p.cfg.AssetName)
|
||||
if asset == nil {
|
||||
continue
|
||||
}
|
||||
if best == nil || compareSemver(r.TagName, best.TagName) > 0 {
|
||||
best = r
|
||||
bestAsset = asset
|
||||
}
|
||||
}
|
||||
if best == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
verification, err := p.fetchVerification(ctx, *bestAsset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rel := &updater.Release{
|
||||
Version: strings.TrimPrefix(strings.TrimPrefix(best.TagName, "v"), "V"),
|
||||
Channel: "stable",
|
||||
Name: best.Name,
|
||||
Notes: best.Body,
|
||||
Artifact: updater.Artifact{
|
||||
Filename: bestAsset.Name,
|
||||
Size: bestAsset.Size,
|
||||
Platform: req.Platform,
|
||||
Arch: req.Arch,
|
||||
},
|
||||
Verification: verification,
|
||||
Metadata: map[string]any{
|
||||
"downloadURL": bestAsset.BrowserDownloadURL,
|
||||
},
|
||||
}
|
||||
if rel.Channel == "stable" && (best.Prerelease) {
|
||||
rel.Channel = "beta"
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, best.PublishedAt); err == nil {
|
||||
rel.PublishedAt = t
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// Download streams the release artifact to dst, reporting progress.
|
||||
func (p *Provider) Download(ctx context.Context, r *updater.Release, dst io.Writer, onProgress func(written, total int64)) error {
|
||||
url, _ := r.Metadata["downloadURL"].(string)
|
||||
if url == "" {
|
||||
return fmt.Errorf("gitea: release %s has no download URL", r.Version)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("User-Agent", "AniTrack-updater")
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("gitea: download %s: %s", url, resp.Status)
|
||||
}
|
||||
total := r.Artifact.Size
|
||||
if total <= 0 {
|
||||
total = resp.ContentLength
|
||||
}
|
||||
var written int64
|
||||
buf := make([]byte, 128*1024)
|
||||
for {
|
||||
n, rerr := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
if _, werr := dst.Write(buf[:n]); werr != nil {
|
||||
return werr
|
||||
}
|
||||
written += int64(n)
|
||||
onProgress(written, total)
|
||||
}
|
||||
if rerr == io.EOF {
|
||||
onProgress(written, total)
|
||||
return nil
|
||||
}
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// listReleases fetches the releases index (newest first page is enough).
|
||||
func (p *Provider) listReleases(ctx context.Context) ([]giteaRelease, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/releases?limit=50", p.cfg.BaseURL, p.cfg.Owner, p.cfg.Repo)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "AniTrack-updater")
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gitea: list releases: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("gitea: list releases: %s", resp.Status)
|
||||
}
|
||||
var releases []giteaRelease
|
||||
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
|
||||
return nil, fmt.Errorf("gitea: decode releases: %w", err)
|
||||
}
|
||||
return releases, nil
|
||||
}
|
||||
|
||||
// findAsset returns the attachment with exactly assetName, or nil.
|
||||
func findAsset(assets []giteaAsset, assetName string) *giteaAsset {
|
||||
for i := range assets {
|
||||
if assets[i].Name == assetName && assets[i].BrowserDownloadURL != "" {
|
||||
return &assets[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchVerification downloads the .sha512 + .sig sidecars next to the asset.
|
||||
// This mirrors `wails3 updater sign` (SHA-512 digest, ed25519ph signature).
|
||||
// Anything missing or malformed is an error: verification fails closed.
|
||||
func (p *Provider) fetchVerification(ctx context.Context, asset giteaAsset) (*updater.Verification, error) {
|
||||
base := strings.TrimSuffix(asset.BrowserDownloadURL, asset.Name)
|
||||
digestHex, err := p.fetchText(ctx, base+asset.Name+".sha512")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gitea: digest sidecar: %w", err)
|
||||
}
|
||||
sigB64, err := p.fetchText(ctx, base+asset.Name+".sig")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gitea: signature sidecar: %w", err)
|
||||
}
|
||||
|
||||
// "<hex> <filename>" (sha512sum format); tolerate bare hex too.
|
||||
digestHex = strings.Fields(digestHex)[0]
|
||||
digest, err := hex.DecodeString(digestHex)
|
||||
if err != nil || len(digest) != 64 {
|
||||
return nil, fmt.Errorf("gitea: malformed digest sidecar for %s", asset.Name)
|
||||
}
|
||||
sig, err := base64.StdEncoding.DecodeString(strings.TrimSpace(sigB64))
|
||||
if err != nil || len(sig) == 0 {
|
||||
return nil, fmt.Errorf("gitea: malformed signature sidecar for %s", asset.Name)
|
||||
}
|
||||
return &updater.Verification{
|
||||
DigestAlgo: "sha512",
|
||||
Digest: digest,
|
||||
SignatureAlgo: "ed25519ph",
|
||||
Signature: sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// fetchText GETs url and returns the trimmed body.
|
||||
func (p *Provider) fetchText(ctx context.Context, url string) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("User-Agent", "AniTrack-updater")
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("%s: %s", url, resp.Status)
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 8*1024))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(body)), nil
|
||||
}
|
||||
|
||||
// compareSemver compares plain versions ("1.99.0", optional v prefix).
|
||||
// Pre-release suffixes (-rc1) sort below the plain release. Returns
|
||||
// -1, 0 or +1. Unparseable input compares as 0.0.0 (never newest).
|
||||
func compareSemver(a, b string) int {
|
||||
an, ap := splitSemver(a)
|
||||
bn, bp := splitSemver(b)
|
||||
for i := 0; i < 3; i++ {
|
||||
if an[i] != bn[i] {
|
||||
if an[i] < bn[i] {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case ap == bp:
|
||||
return 0
|
||||
case ap == "":
|
||||
return 1
|
||||
case bp == "":
|
||||
return -1
|
||||
case ap < bp:
|
||||
return -1
|
||||
case ap > bp:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// splitSemver splits "v1.99.0-rc1" into ([1 99 0], "rc1").
|
||||
func splitSemver(v string) ([3]int, string) {
|
||||
var nums [3]int
|
||||
v = strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(v), "v"), "V")
|
||||
core, pre, _ := strings.Cut(v, "-")
|
||||
for i, part := range strings.Split(core, ".") {
|
||||
if i >= 3 {
|
||||
break
|
||||
}
|
||||
n, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
return [3]int{}, pre
|
||||
}
|
||||
nums[i] = n
|
||||
}
|
||||
return nums, pre
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/updater"
|
||||
)
|
||||
|
||||
func TestCompareSemver(t *testing.T) {
|
||||
cases := []struct {
|
||||
a, b string
|
||||
want int
|
||||
}{
|
||||
{"1.99.0", "1.6.8", 1},
|
||||
{"1.6.8", "1.99.0", -1},
|
||||
{"1.99.0", "1.99.0", 0},
|
||||
{"v1.99.0", "1.99.0", 0},
|
||||
{"2.0.0", "1.99.9", 1},
|
||||
{"1.99.0-rc1", "1.99.0", -1},
|
||||
{"1.99.0", "1.99.0-rc1", 1},
|
||||
{"1.99.0-rc1", "1.99.0-rc2", -1},
|
||||
{"1.10.0", "1.9.0", 1}, // numeric, not lexicographic
|
||||
{"garbage", "1.0.0", -1},
|
||||
{"1.0.0", "garbage", 1},
|
||||
{"", "", 0},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := compareSemver(c.a, c.b); got != c.want {
|
||||
t.Errorf("compareSemver(%q, %q) = %d, want %d", c.a, c.b, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const testAsset = "AniTrack-linux-amd64"
|
||||
const testData = "artifact-bytes"
|
||||
|
||||
type releaseSpec struct {
|
||||
tag string
|
||||
draft bool
|
||||
prerelease bool
|
||||
withAsset bool
|
||||
withSidecar bool
|
||||
}
|
||||
|
||||
// buildServer serves a canned releases index. Every spec with withAsset gets
|
||||
// the test artifact; sidecars (.sha512/.sig, correctly formed) are served
|
||||
// only if at least one spec sets withSidecar.
|
||||
func buildServer(t *testing.T, specs []releaseSpec) *httptest.Server {
|
||||
t.Helper()
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data := []byte(testData)
|
||||
sum := sha512.Sum512(data)
|
||||
sha := hex.EncodeToString(sum[:]) + " " + testAsset + "\n"
|
||||
sig := base64.StdEncoding.EncodeToString(ed25519.Sign(priv, sum[:])) + "\n"
|
||||
serveSidecars := false
|
||||
for _, s := range specs {
|
||||
if s.withSidecar {
|
||||
serveSidecars = true
|
||||
}
|
||||
}
|
||||
var srv *httptest.Server
|
||||
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, "/releases"):
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
var b strings.Builder
|
||||
b.WriteString("[")
|
||||
for i, s := range specs {
|
||||
assets := ""
|
||||
if s.withAsset {
|
||||
assets = fmt.Sprintf(`{"name":%q,"size":%d,"browser_download_url":%q}`,
|
||||
testAsset, len(data), srv.URL+"/dl/"+testAsset)
|
||||
}
|
||||
if i > 0 {
|
||||
b.WriteString(",")
|
||||
}
|
||||
fmt.Fprintf(&b, `{"tag_name":%q,"name":%q,"body":"notes","draft":%v,"prerelease":%v,"published_at":"2026-09-16T00:00:00Z","assets":[%s]}`,
|
||||
s.tag, s.tag, s.draft, s.prerelease, assets)
|
||||
}
|
||||
b.WriteString("]")
|
||||
fmt.Fprint(w, b.String())
|
||||
case serveSidecars && r.URL.Path == "/dl/"+testAsset+".sha512":
|
||||
fmt.Fprint(w, sha)
|
||||
case serveSidecars && r.URL.Path == "/dl/"+testAsset+".sig":
|
||||
fmt.Fprint(w, sig)
|
||||
case serveSidecars && r.URL.Path == "/dl/"+testAsset:
|
||||
w.Write(data)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
return srv
|
||||
}
|
||||
|
||||
func testProvider(srv *httptest.Server, allowPre bool) *Provider {
|
||||
p, err := New(Config{
|
||||
BaseURL: srv.URL,
|
||||
Owner: "o",
|
||||
Repo: "r",
|
||||
AssetName: testAsset,
|
||||
AllowPrerelease: allowPre,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func TestCheckFindsNewest(t *testing.T) {
|
||||
srv := buildServer(t, []releaseSpec{
|
||||
{tag: "1.6.8", withAsset: true, withSidecar: true},
|
||||
{tag: "1.99.0", withAsset: true, withSidecar: true},
|
||||
})
|
||||
defer srv.Close()
|
||||
rel, err := testProvider(srv, false).Check(context.Background(), updater.CheckRequest{CurrentVersion: "1.6.8"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rel == nil {
|
||||
t.Fatal("expected a release, got up-to-date")
|
||||
}
|
||||
if rel.Version != "1.99.0" {
|
||||
t.Errorf("version = %q, want 1.99.0", rel.Version)
|
||||
}
|
||||
if rel.Verification == nil || rel.Verification.DigestAlgo != "sha512" || rel.Verification.SignatureAlgo != "ed25519ph" {
|
||||
t.Errorf("verification not populated: %+v", rel.Verification)
|
||||
}
|
||||
if len(rel.Verification.Digest) != 64 || len(rel.Verification.Signature) == 0 {
|
||||
t.Errorf("verification malformed: %+v", rel.Verification)
|
||||
}
|
||||
if rel.Notes != "notes" {
|
||||
t.Errorf("notes = %q", rel.Notes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckUpToDate(t *testing.T) {
|
||||
srv := buildServer(t, []releaseSpec{{tag: "1.99.0", withAsset: true, withSidecar: true}})
|
||||
defer srv.Close()
|
||||
rel, err := testProvider(srv, false).Check(context.Background(), updater.CheckRequest{CurrentVersion: "1.99.0"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rel != nil {
|
||||
t.Errorf("expected up-to-date, got %s", rel.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckSkipsDraftPrereleaseAndMissingAsset(t *testing.T) {
|
||||
srv := buildServer(t, []releaseSpec{
|
||||
{tag: "1.99.2", draft: true, withAsset: true, withSidecar: true},
|
||||
{tag: "1.99.1", prerelease: true, withAsset: true, withSidecar: true},
|
||||
{tag: "1.99.0", withAsset: false},
|
||||
{tag: "1.6.9", withAsset: true, withSidecar: true},
|
||||
})
|
||||
defer srv.Close()
|
||||
rel, err := testProvider(srv, false).Check(context.Background(), updater.CheckRequest{CurrentVersion: "1.6.8"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rel == nil || rel.Version != "1.6.9" {
|
||||
t.Fatalf("version = %v, want 1.6.9 (draft/prerelease/asset-less skipped)", rel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckAllowPrerelease(t *testing.T) {
|
||||
srv := buildServer(t, []releaseSpec{
|
||||
{tag: "1.99.1-rc1", prerelease: true, withAsset: true, withSidecar: true},
|
||||
})
|
||||
defer srv.Close()
|
||||
rel, err := testProvider(srv, true).Check(context.Background(), updater.CheckRequest{CurrentVersion: "1.99.0"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rel == nil || rel.Version != "1.99.1-rc1" {
|
||||
t.Fatalf("version = %v, want 1.99.1-rc1", rel)
|
||||
}
|
||||
if rel.Channel != "beta" {
|
||||
t.Errorf("channel = %q, want beta", rel.Channel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckFailsClosedWithoutSidecars(t *testing.T) {
|
||||
srv := buildServer(t, []releaseSpec{{tag: "1.99.0", withAsset: true, withSidecar: false}})
|
||||
defer srv.Close()
|
||||
_, err := testProvider(srv, false).Check(context.Background(), updater.CheckRequest{CurrentVersion: "1.6.8"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing sidecars, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckServerError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "boom", http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
_, err := testProvider(srv, false).Check(context.Background(), updater.CheckRequest{CurrentVersion: "1.6.8"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 500, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadStreamsWithProgress(t *testing.T) {
|
||||
srv := buildServer(t, []releaseSpec{{tag: "1.99.0", withAsset: true, withSidecar: true}})
|
||||
defer srv.Close()
|
||||
p := testProvider(srv, false)
|
||||
rel, err := p.Check(context.Background(), updater.CheckRequest{CurrentVersion: "1.6.8"})
|
||||
if err != nil || rel == nil {
|
||||
t.Fatalf("check: %v %v", rel, err)
|
||||
}
|
||||
f, err := os.CreateTemp("", "dl")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
var calls int
|
||||
var last int64
|
||||
err = p.Download(context.Background(), rel, f, func(written, total int64) {
|
||||
calls++
|
||||
last = written
|
||||
if total != int64(len(testData)) {
|
||||
t.Errorf("total = %d", total)
|
||||
}
|
||||
})
|
||||
f.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if calls == 0 || last != int64(len(testData)) {
|
||||
t.Errorf("progress calls=%d last=%d", calls, last)
|
||||
}
|
||||
got, _ := os.ReadFile(f.Name())
|
||||
if string(got) != testData {
|
||||
t.Errorf("body = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewValidatesConfig(t *testing.T) {
|
||||
if _, err := New(Config{}); err == nil {
|
||||
t.Error("expected error for empty config")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLiveGiteaCheck hits the real instance. Opt-in only:
|
||||
// ANITRACK_LIVE_TEST=1 go test ./updater/gitea/ -run TestLiveGiteaCheck -v
|
||||
func TestLiveGiteaCheck(t *testing.T) {
|
||||
if os.Getenv("ANITRACK_LIVE_TEST") == "" {
|
||||
t.Skip("set ANITRACK_LIVE_TEST=1 to run")
|
||||
}
|
||||
p, err := New(Config{
|
||||
BaseURL: "https://git.linuxhg.com",
|
||||
Owner: "john-okeefe",
|
||||
Repo: "Anitrack",
|
||||
AssetName: "AniTrack-linux-amd64",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rel, err := p.Check(context.Background(), updater.CheckRequest{CurrentVersion: "0.0.0"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rel == nil {
|
||||
// Correct until CI publishes the bare-binary asset: no release
|
||||
// carries AniTrack-linux-amd64 yet, so there is nothing to offer.
|
||||
// This still proves API reachability + response parsing live.
|
||||
t.Log("no matching asset published yet (expected until CI ships AniTrack-linux-amd64)")
|
||||
return
|
||||
}
|
||||
t.Logf("latest=%s notes=%d bytes", rel.Version, len(rel.Notes))
|
||||
var n int64
|
||||
err = p.Download(context.Background(), rel, io.Discard, func(w, _ int64) { n = w })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("downloaded %d bytes", n)
|
||||
}
|
||||
Reference in New Issue
Block a user