wraith/internal/updater/service_test.go
Vantz Stockwell f22fbe14fa
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 1m5s
feat: auto-updater — check Gitea packages for new versions, download + install
Add internal/updater package with UpdateService that queries the Gitea
generic-package API for newer releases, downloads the installer with
SHA256 verification, and launches it to apply the update. Includes
semver comparison (CompareVersions) and comprehensive test coverage
with httptest-based mock servers.

Wire UpdateService into WraithApp (app.go accepts version param) and
register as a Wails service in main.go. Frontend StatusBar shows a
blue pill notification when an update is available; SettingsModal About
section displays the current version and a "Check for Updates" button
with idle/checking/found/up-to-date/error states.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 10:11:50 -04:00

224 lines
6.0 KiB
Go

package updater
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestNewUpdateService(t *testing.T) {
svc := NewUpdateService("0.2.0")
if svc.currentVersion != "0.2.0" {
t.Errorf("currentVersion = %q, want %q", svc.currentVersion, "0.2.0")
}
if svc.baseURL == "" {
t.Error("baseURL should not be empty")
}
if svc.owner == "" {
t.Error("owner should not be empty")
}
if svc.pkg == "" {
t.Error("pkg should not be empty")
}
if svc.httpClient == nil {
t.Error("httpClient should not be nil")
}
}
func TestNewUpdateServiceStripsVPrefix(t *testing.T) {
svc := NewUpdateService("v1.2.3")
if svc.currentVersion != "1.2.3" {
t.Errorf("currentVersion = %q, want %q", svc.currentVersion, "1.2.3")
}
}
func TestCompareVersions(t *testing.T) {
tests := []struct {
a, b string
want int
}{
{"0.2.0", "0.2.1", -1},
{"0.2.1", "0.2.0", 1},
{"0.3.0", "0.2.9", 1},
{"0.2.9", "0.3.0", -1},
{"1.0.0", "0.9.9", 1},
{"0.9.9", "1.0.0", -1},
{"1.0.0", "1.0.0", 0},
{"0.0.1", "0.0.1", 0},
{"2.0.0", "1.99.99", 1},
{"0.1.0", "0.0.99", 1},
// With "v" prefix
{"v0.2.0", "v0.2.1", -1},
{"v1.0.0", "1.0.0", 0},
}
for _, tt := range tests {
got := CompareVersions(tt.a, tt.b)
if got != tt.want {
t.Errorf("CompareVersions(%q, %q) = %d, want %d", tt.a, tt.b, got, tt.want)
}
}
}
func TestParseSemver(t *testing.T) {
tests := []struct {
input string
want [3]int
}{
{"1.2.3", [3]int{1, 2, 3}},
{"0.0.0", [3]int{0, 0, 0}},
{"10.20.30", [3]int{10, 20, 30}},
{"1.2", [3]int{1, 2, 0}},
{"1", [3]int{1, 0, 0}},
{"", [3]int{0, 0, 0}},
}
for _, tt := range tests {
got := parseSemver(tt.input)
if got != tt.want {
t.Errorf("parseSemver(%q) = %v, want %v", tt.input, got, tt.want)
}
}
}
func TestParseVersionJSON(t *testing.T) {
raw := `{"version":"0.3.0","sha256":"abcdef1234567890","downloadUrl":"https://example.com/setup.exe"}`
var v versionJSON
if err := json.Unmarshal([]byte(raw), &v); err != nil {
t.Fatalf("Unmarshal error: %v", err)
}
if v.Version != "0.3.0" {
t.Errorf("Version = %q, want %q", v.Version, "0.3.0")
}
if v.SHA256 != "abcdef1234567890" {
t.Errorf("SHA256 = %q, want %q", v.SHA256, "abcdef1234567890")
}
if v.DownloadURL != "https://example.com/setup.exe" {
t.Errorf("DownloadURL = %q, want %q", v.DownloadURL, "https://example.com/setup.exe")
}
}
func TestCheckForUpdate_NewerAvailable(t *testing.T) {
// Mock the Gitea API: return a single package version that is newer.
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/packages/vstockwell/generic/wraith", func(w http.ResponseWriter, r *http.Request) {
versions := []giteaPackageVersion{{Version: "0.3.0"}}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(versions)
})
mux.HandleFunc("/api/packages/vstockwell/generic/wraith/0.3.0/version.json", func(w http.ResponseWriter, r *http.Request) {
v := versionJSON{
Version: "0.3.0",
SHA256: "abc123",
DownloadURL: "https://example.com/wraith-0.3.0-setup.exe",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
})
server := httptest.NewServer(mux)
defer server.Close()
svc := NewUpdateService("0.2.0")
svc.baseURL = server.URL
info, err := svc.CheckForUpdate()
if err != nil {
t.Fatalf("CheckForUpdate() error: %v", err)
}
if !info.Available {
t.Error("expected Available = true")
}
if info.LatestVer != "0.3.0" {
t.Errorf("LatestVer = %q, want %q", info.LatestVer, "0.3.0")
}
if info.SHA256 != "abc123" {
t.Errorf("SHA256 = %q, want %q", info.SHA256, "abc123")
}
if info.DownloadURL != "https://example.com/wraith-0.3.0-setup.exe" {
t.Errorf("DownloadURL = %q, want %q", info.DownloadURL, "https://example.com/wraith-0.3.0-setup.exe")
}
}
func TestCheckForUpdate_AlreadyUpToDate(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/packages/vstockwell/generic/wraith", func(w http.ResponseWriter, r *http.Request) {
versions := []giteaPackageVersion{{Version: "0.2.0"}}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(versions)
})
server := httptest.NewServer(mux)
defer server.Close()
svc := NewUpdateService("0.2.0")
svc.baseURL = server.URL
info, err := svc.CheckForUpdate()
if err != nil {
t.Fatalf("CheckForUpdate() error: %v", err)
}
if info.Available {
t.Error("expected Available = false (same version)")
}
}
func TestCheckForUpdate_CurrentIsNewer(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/packages/vstockwell/generic/wraith", func(w http.ResponseWriter, r *http.Request) {
versions := []giteaPackageVersion{{Version: "0.1.0"}}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(versions)
})
server := httptest.NewServer(mux)
defer server.Close()
svc := NewUpdateService("0.2.0")
svc.baseURL = server.URL
info, err := svc.CheckForUpdate()
if err != nil {
t.Fatalf("CheckForUpdate() error: %v", err)
}
if info.Available {
t.Error("expected Available = false (current is newer)")
}
}
func TestCheckForUpdate_NoVersions(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/packages/vstockwell/generic/wraith", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode([]giteaPackageVersion{})
})
server := httptest.NewServer(mux)
defer server.Close()
svc := NewUpdateService("0.2.0")
svc.baseURL = server.URL
info, err := svc.CheckForUpdate()
if err != nil {
t.Fatalf("CheckForUpdate() error: %v", err)
}
if info.Available {
t.Error("expected Available = false (no versions)")
}
}
func TestCheckForUpdate_APIError(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/packages/vstockwell/generic/wraith", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
})
server := httptest.NewServer(mux)
defer server.Close()
svc := NewUpdateService("0.2.0")
svc.baseURL = server.URL
_, err := svc.CheckForUpdate()
if err == nil {
t.Error("expected error from API failure")
}
}