- Paket internal/update: ambil rilis terbaru (API GitHub), bandingkan versi semver sederhana, unduh AppImage ke berkas sementara lalu tukar atomik (verifikasi magic ELF+AI; kalau gagal, AppImage lama tetap utuh) - UI: cek otomatis 2 detik setelah aplikasi tampil (hanya kalau jalan sebagai AppImage), dialog notifikasi versi baru + catatan rilis, tombol Update now / Halaman rilis / Nanti - Pemasangan: lewat AppImageUpdate kalau terpasang, kalau tidak unduh sendiri; setelah selesai ditawarkan Restart (jalankan AppImage baru lalu tutup yang lama) - Tombol footer Cek Update memakai alur yang sama dan selalu memberi jawaban - LAZYRUN_UPDATE_API untuk mengarahkan cek update ke server lain (tes/fork) - Tes: perbandingan versi, parsing rilis, penukaran AppImage, penolakan berkas bukan AppImage
181 lines
4.7 KiB
Go
181 lines
4.7 KiB
Go
package update
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestCompareVersions(t *testing.T) {
|
|
cases := []struct {
|
|
a, b string
|
|
want int
|
|
}{
|
|
{"1.0.0", "1.0.0", 0},
|
|
{"v1.0.0", "1.0.0", 0},
|
|
{"1.1.0", "1.0.0", 1},
|
|
{"1.0.0", "1.1.0", -1},
|
|
{"1.10.0", "1.9.0", 1},
|
|
{"2.0", "1.9.9", 1},
|
|
{"1.0", "1.0.0", 0},
|
|
{"1.0.0-rc1", "1.0.0", -1},
|
|
{"1.0.0", "1.0.0-rc1", 1},
|
|
{"dev", "1.0.0", -1},
|
|
}
|
|
for _, c := range cases {
|
|
if got := CompareVersions(c.a, c.b); got != c.want {
|
|
t.Errorf("CompareVersions(%q, %q) = %d, mau %d", c.a, c.b, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewerIgnoresSameAndOlder(t *testing.T) {
|
|
if Newer("1.0.0", Release{Version: "1.0.0"}) {
|
|
t.Error("versi sama bukan update")
|
|
}
|
|
if Newer("1.0.0", Release{Version: "0.9.0"}) {
|
|
t.Error("versi lama bukan update")
|
|
}
|
|
if !Newer("1.0.0", Release{Version: "1.0.1"}) {
|
|
t.Error("versi lebih baru harus terdeteksi")
|
|
}
|
|
}
|
|
|
|
func TestLatestParsesRelease(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("User-Agent") == "" {
|
|
t.Error("GitHub butuh header User-Agent")
|
|
}
|
|
_, _ = w.Write([]byte(`{
|
|
"tag_name": "v1.2.0",
|
|
"body": " - ikon online\n - animasi ",
|
|
"html_url": "https://github.com/oxpoe/lazyrun/releases/tag/v1.2.0",
|
|
"assets": [
|
|
{"name": "lazyrun-1.2.0-x86_64.AppImage.zsync", "browser_download_url": "https://contoh/zsync"},
|
|
{"name": "lazyrun-1.2.0-x86_64.AppImage", "browser_download_url": "https://contoh/appimage"}
|
|
]
|
|
}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
SetEndpoint(srv.URL)
|
|
t.Cleanup(func() { SetEndpoint("https://api.github.com/repos/oxpoe/lazyrun/releases") })
|
|
|
|
rel, err := Latest()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rel.Version != "1.2.0" || rel.Tag != "v1.2.0" {
|
|
t.Errorf("versi = %q / %q", rel.Version, rel.Tag)
|
|
}
|
|
if rel.AppImage != "https://contoh/appimage" || rel.Zsync != "https://contoh/zsync" {
|
|
t.Errorf("aset salah: %q %q", rel.AppImage, rel.Zsync)
|
|
}
|
|
if rel.Notes != "- ikon online\n - animasi" {
|
|
t.Errorf("catatan rilis = %q", rel.Notes)
|
|
}
|
|
if !Newer("1.1.9", rel) {
|
|
t.Error("1.2.0 harus dianggap lebih baru dari 1.1.9")
|
|
}
|
|
}
|
|
|
|
func TestDownloadReplacesAppImageAtomically(t *testing.T) {
|
|
appImage := fakeAppImage(2048)
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write(appImage)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
target := filepath.Join(t.TempDir(), "lazyrun.AppImage")
|
|
if err := os.WriteFile(target, fakeAppImage(1024), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := Download(Release{AppImage: srv.URL}, target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
data, err := os.ReadFile(target)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(data) != string(appImage) {
|
|
t.Error("isi AppImage tidak terganti")
|
|
}
|
|
info, err := os.Stat(target)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Mode().Perm()&0o111 == 0 {
|
|
t.Error("AppImage hasil update harus executable")
|
|
}
|
|
// tidak boleh menyisakan berkas sementara
|
|
entries, err := os.ReadDir(filepath.Dir(target))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(entries) != 1 {
|
|
t.Errorf("ada berkas sisa: %v", entries)
|
|
}
|
|
}
|
|
|
|
func TestDownloadKeepsOldAppImageOnBadData(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte("<html>404</html>"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
target := filepath.Join(t.TempDir(), "lazyrun.AppImage")
|
|
old := fakeAppImage(512)
|
|
if err := os.WriteFile(target, old, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := Download(Release{AppImage: srv.URL}, target); err == nil {
|
|
t.Fatal("berkas bukan AppImage harus ditolak")
|
|
}
|
|
data, err := os.ReadFile(target)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(data) != string(old) {
|
|
t.Error("AppImage lama harus tetap utuh saat update gagal")
|
|
}
|
|
}
|
|
|
|
func TestDownloadWithoutAsset(t *testing.T) {
|
|
target := filepath.Join(t.TempDir(), "lazyrun.AppImage")
|
|
if err := Download(Release{}, target); err == nil {
|
|
t.Error("rilis tanpa AppImage harus error")
|
|
}
|
|
}
|
|
|
|
func TestIsAppImage(t *testing.T) {
|
|
dir := t.TempDir()
|
|
good := filepath.Join(dir, "good")
|
|
if err := os.WriteFile(good, fakeAppImage(16), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !IsAppImage(good) {
|
|
t.Error("AppImage sah tidak dikenali")
|
|
}
|
|
|
|
plain := filepath.Join(dir, "plain")
|
|
if err := os.WriteFile(plain, []byte("\x7fELF\x02\x01\x01\x00no-magic-here"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if IsAppImage(plain) {
|
|
t.Error("ELF biasa bukan AppImage")
|
|
}
|
|
}
|
|
|
|
// fakeAppImage membuat berkas dengan magic ELF + AI (cukup untuk verifikasi).
|
|
func fakeAppImage(size int) []byte {
|
|
data := make([]byte, size)
|
|
copy(data, []byte{0x7f, 'E', 'L', 'F', 0x02, 0x01, 0x01, 0x00})
|
|
copy(data[8:], []byte{'A', 'I', 0x02})
|
|
return data
|
|
}
|