package update import ( "context" "crypto/sha256" "encoding/hex" "errors" "io" "log/slog" "os" "path/filepath" "testing" "internetpressure.io/probe-agent/internal/protocol" ) type fakeFetcher struct { latest protocol.AgentLatest asset []byte downloads int } func (f *fakeFetcher) AgentLatest(context.Context) (*protocol.AgentLatest, error) { return &f.latest, nil } func (f *fakeFetcher) Download(_ context.Context, _ string, w io.Writer, _ int64) (int64, error) { f.downloads++ n, err := w.Write(f.asset) return int64(n), err } func TestUpdateReplacesBinary(t *testing.T) { dir := t.TempDir() exe := filepath.Join(dir, "bin", "ip-probe") os.MkdirAll(filepath.Dir(exe), 0o755) os.WriteFile(exe, []byte("old"), 0o755) newBin := []byte("#!/bin/sh\necho new\n") sum := sha256.Sum256(newBin) f := &fakeFetcher{asset: newBin, latest: protocol.AgentLatest{Version: "0.2.0", Assets: map[string]protocol.Asset{Platform(): {URL: "https://x/ip-probe", SHA256: hex.EncodeToString(sum[:])}}}} u := &Updater{Fetcher: f, Version: "0.1.0", DataDir: dir, ExePath: exe, Log: slog.New(slog.DiscardHandler)} updated, err := u.Check(context.Background()) if err != nil || !updated { t.Fatalf("updated=%v err=%v", updated, err) } got, _ := os.ReadFile(exe) if string(got) != string(newBin) { t.Fatalf("binary not replaced: %q", got) } if fi, _ := os.Stat(exe); fi.Mode().Perm() != 0o755 { t.Fatalf("mode %v", fi.Mode()) } if _, err := os.Stat(filepath.Join(dir, tmpFile)); !errors.Is(err, os.ErrNotExist) { t.Fatal("tmp file left behind") } // Same version offered again (e.g. the new binary crashes and the old one is restored) → not re-attempted. os.WriteFile(exe, []byte("old"), 0o755) updated, err = u.Check(context.Background()) if err != nil || updated || f.downloads != 1 { t.Fatalf("loop guard failed: updated=%v err=%v downloads=%d", updated, err, f.downloads) } } func TestUpdateRejectsBadHashAndSameVersion(t *testing.T) { dir := t.TempDir() exe := filepath.Join(dir, "ip-probe") os.WriteFile(exe, []byte("old"), 0o755) f := &fakeFetcher{asset: []byte("evil"), latest: protocol.AgentLatest{Version: "0.3.0", Assets: map[string]protocol.Asset{Platform(): {URL: "u", SHA256: "00"}}}} u := &Updater{Fetcher: f, Version: "0.1.0", DataDir: dir, ExePath: exe, Log: slog.New(slog.DiscardHandler)} updated, err := u.Check(context.Background()) if updated || err == nil { t.Fatalf("bad hash accepted: updated=%v err=%v", updated, err) } if got, _ := os.ReadFile(exe); string(got) != "old" { t.Fatal("binary modified despite bad hash") } // Same version → nothing to do, no download. f.latest.Version = "0.1.0" f.downloads = 0 if updated, err := u.Check(context.Background()); updated || err != nil || f.downloads != 0 { t.Fatalf("same version: updated=%v err=%v downloads=%d", updated, err, f.downloads) } // No asset for this platform → skip quietly. f.latest = protocol.AgentLatest{Version: "0.4.0", Assets: map[string]protocol.Asset{"plan9-mips": {URL: "u", SHA256: "00"}}} if updated, err := u.Check(context.Background()); updated || err != nil { t.Fatalf("missing platform: updated=%v err=%v", updated, err) } }