// End-to-end updater flow test (headless): Gitea fixture provider -> // framework Check -> DownloadAndInstall -> REAL signature verification // against a test key. Proves our sidecar shape is exactly what the // framework verifier accepts. No network, no display, no Run(). package main import ( "context" "crypto" "crypto/ed25519" "crypto/rand" "crypto/sha512" "encoding/base64" "encoding/hex" "fmt" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/updater" giteaprovider "AniTrack/updater/gitea" ) const flowAsset = "AniTrack-linux-amd64" const flowData = "flow-test-binary-bytes" // flowServer serves one signed release for version 1.99.0. When *tampered // is true the .sig sidecar is corrupted so verification must fail. The flag // is read per-request so one server covers both phases (the framework keeps // a process-wide app singleton, so only one application.New per test binary). func flowServer(t *testing.T, pub ed25519.PublicKey, priv ed25519.PrivateKey, tampered *bool) *httptest.Server { t.Helper() data := []byte(flowData) sum := sha512.Sum512(data) sigBytes, err := priv.Sign(rand.Reader, sum[:], &ed25519.Options{Hash: crypto.SHA512}) if err != nil { t.Fatal(err) } sha := hex.EncodeToString(sum[:]) + " " + flowAsset + "\n" mkSig := func() string { s := sigBytes if *tampered { s = append([]byte(nil), sigBytes...) s[0] ^= 0xff } return base64.StdEncoding.EncodeToString(s) + "\n" } var srv *httptest.Server srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.HasSuffix(r.URL.Path, "/releases"): fmt.Fprintf(w, `[{"tag_name":"1.99.0","name":"1.99.0","body":"n","draft":false,"prerelease":false,"published_at":"2026-09-16T00:00:00Z","assets":[{"name":%q,"size":%d,"browser_download_url":%q}]}]`, flowAsset, len(data), srv.URL+"/dl/"+flowAsset) case r.URL.Path == "/dl/"+flowAsset+".sha512": fmt.Fprint(w, sha) case r.URL.Path == "/dl/"+flowAsset+".sig": fmt.Fprint(w, mkSig()) case r.URL.Path == "/dl/"+flowAsset: w.Write(data) default: http.NotFound(w, r) } })) _ = pub return srv } func TestUpdaterFlowDownloadsAndVerifies(t *testing.T) { pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } _ = priv tampered := false srv := flowServer(t, pub, priv, &tampered) defer srv.Close() gh, err := giteaprovider.New(giteaprovider.Config{ BaseURL: srv.URL, Owner: "o", Repo: "r", AssetName: flowAsset, }) if err != nil { t.Fatal(err) } app := application.New(application.Options{Name: "AniTrackFlowTest"}) if err := app.Updater.Init(updater.Config{ CurrentVersion: "1.6.8", Providers: []updater.Provider{gh}, PublicKey: []byte(pub), Window: updater.WindowNone, }); err != nil { t.Fatal(err) } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() // Phase 1: good signature -> staged bytes match the fixture. rel, err := app.Updater.Check(ctx) if err != nil { t.Fatal(err) } if rel == nil { t.Fatal("expected an update from 1.6.8 to 1.99.0") } if err := app.Updater.DownloadAndInstall(ctx); err != nil { t.Fatalf("DownloadAndInstall (incl. real signature verify): %s", err) } staged := app.Updater.DownloadedPath() if staged == "" { t.Fatal("no staged path after install") } got, err := os.ReadFile(staged) if err != nil { t.Fatal(err) } if string(got) != flowData { t.Errorf("staged bytes = %q, want fixture", got) } os.Remove(staged) // Phase 2: tampered signature -> install must fail closed. tampered = true if _, err := app.Updater.Check(ctx); err != nil { t.Fatal(err) } if err := app.Updater.DownloadAndInstall(ctx); err == nil { t.Fatal("expected verification failure for tampered signature, got nil") } else { t.Logf("rejected as expected: %s", err) } }