package update import ( "context" "errors" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) // mockGitea поднимает httptest-сервер с бинарём, .version и .sha256. func mockGitea(t *testing.T, bin []byte, version, checksum string) *httptest.Server { t.Helper() name := PlatformFilename() mux := http.NewServeMux() mux.HandleFunc("/api/packages/", func(w http.ResponseWriter, r *http.Request) { base := name switch { case strings.HasSuffix(r.URL.Path, ".version"): base = name + ".version" _, _ = w.Write([]byte(version)) case strings.HasSuffix(r.URL.Path, ".sha256"): base = name + ".sha256" _, _ = w.Write([]byte(checksum)) default: _, _ = w.Write(bin) // сам бинарь } _ = base }) return httptest.NewServer(mux) } // testUpdater строит Updater с базой на мок-сервере и каталогом tmp. func testUpdater(t *testing.T, bin []byte, version, cur string) (*Updater, string) { t.Helper() sum := sha256hex(bin) srv := mockGitea(t, bin, version, sum) t.Cleanup(srv.Close) dir := t.TempDir() u := &Updater{ BaseURL: srv.URL, Owner: "kamelion", Package: "ratatoskr", CurrentVersion: cur, Dir: dir, } return u, dir } func TestFilename(t *testing.T) { cases := []struct{ goos, goarch, want string }{ {"linux", "amd64", "ratatoskr-linux-amd64"}, {"linux", "arm64", "ratatoskr-linux-arm64"}, {"darwin", "arm64", "ratatoskr-darwin-arm64"}, {"windows", "amd64", "ratatoskr-windows-amd64.exe"}, } for _, c := range cases { if got := Filename(c.goos, c.goarch); got != c.want { t.Errorf("Filename(%s,%s)=%q want %q", c.goos, c.goarch, got, c.want) } } } func TestCheck_UpdateAvailable(t *testing.T) { u, _ := testUpdater(t, []byte("bin"), "commit-abc1234", "commit-old0000") res := u.Check(context.Background()) if res.Err != nil { t.Fatalf("Check err = %v", res.Err) } if !res.UpdateAvailable { t.Fatal("expected update available") } if res.Version != "commit-abc1234" { t.Errorf("Version = %q want commit-abc1234", res.Version) } } func TestCheck_AlreadyCurrent(t *testing.T) { u, _ := testUpdater(t, []byte("bin"), "commit-abc1234", "commit-abc1234") res := u.Check(context.Background()) if res.Err != nil { t.Fatalf("Check err = %v", res.Err) } if res.UpdateAvailable { t.Fatal("not expected update (same version)") } } func TestCheck_ServerDown(t *testing.T) { dir := t.TempDir() u := &Updater{BaseURL: "http://127.0.0.1:1", Owner: "k", Package: "p", CurrentVersion: "v1", Dir: dir} res := u.Check(context.Background()) if res.Err == nil { t.Fatal("expected error for unreachable server") } // ошибка должна классифицироваться как U1 ErrCheckFailed var uerr *Error if !errorsAs(res.Err, &uerr) || uerr.Code != U1 { t.Errorf("expected U1 error, got %v", res.Err) } } func TestDownload_And_Verify_Good(t *testing.T) { bin := []byte("ratatoskr-binary-content-v2") u, dir := testUpdater(t, bin, "commit-new12345", "") file, err := u.Download(context.Background(), "commit-new12345") if err != nil { t.Fatalf("Download err = %v", err) } if !strings.Contains(file, ".new") { t.Errorf("temp file %q should end with .new", filepath.Base(file)) } if err := u.Verify(context.Background(), file); err != nil { t.Fatalf("Verify err = %v", err) } // .new лежит в каталоге entries, _ := os.ReadDir(dir) if len(entries) != 1 || !strings.HasSuffix(entries[0].Name(), ".new") { t.Errorf("expected 1 .new file in dir, got %v", entries) } } func TestVerify_ChecksumMismatch(t *testing.T) { bin := []byte("A") srv := mockGitea(t, bin, "commit-x", sha256hex([]byte("differente-content"))) t.Cleanup(srv.Close) u := &Updater{BaseURL: srv.URL, Owner: "k", Package: "p", Dir: t.TempDir()} file := filepath.Join(u.Dir, ".ratatoskr.x.new") if err := os.WriteFile(file, []byte("A"), 0o755); err != nil { t.Fatal(err) } err := u.Verify(context.Background(), file) if err == nil { t.Fatal("expected checksum mismatch error") } var uerr *Error if !errorsAs(err, &uerr) || uerr.Code != U4 { t.Errorf("expected U4 error, got %v", err) } } func TestSanitizeVer(t *testing.T) { cases := map[string]string{ "commit-abc1234": "commit-abc1234", "v1.2.3": "v1.2.3", "a/b c": "a_b_c", "": "unknown", } for in, want := range cases { if got := sanitizeVer(in); got != want { t.Errorf("sanitizeVer(%q)=%q want %q", in, got, want) } } } func TestApplySwap_NoPending(t *testing.T) { dir := t.TempDir() u := &Updater{Dir: dir} if err := u.applySwap(dir, filepath.Join(dir, "ratatoskr")); err != nil { t.Fatalf("applySwap with no .new should return nil, got %v", err) } } func TestApplySwap_Applies(t *testing.T) { dir := t.TempDir() exe := filepath.Join(dir, "ratatoskr") if err := os.WriteFile(exe, []byte("old"), 0o755); err != nil { t.Fatal(err) } newFile := filepath.Join(dir, ".ratatoskr.commit-n.new") if err := os.WriteFile(newFile, []byte("new-content"), 0o755); err != nil { t.Fatal(err) } u := &Updater{Dir: dir} if err := u.applySwap(dir, exe); err != nil { t.Fatalf("applySwap err = %v", err) } got, _ := os.ReadFile(exe) if string(got) != "new-content" { t.Errorf("exe content = %q want new-content", got) } // старый бэкап в .old old, _ := os.ReadFile(filepath.Join(dir, CrashSafeTag)) if string(old) != "old" { t.Errorf(".old content = %q want old", old) } } func TestErrorsAs(t *testing.T) { // проверка классификатора через errorsAs на Error(U4) err := ue(U4, "verify", os.ErrPermission) var uerr *Error if !errorsAs(err, &uerr) { t.Fatal("errorsAs failed to unwrap *Error") } if uerr.Code != U4 { t.Errorf("code = %v want U4", uerr.Code) } if !errors.Is(err, os.ErrPermission) { t.Errorf("errors.Is(err, ErrPermission) = false, want true") } } // errorsAs — тонкая обёртка errors.As для классификатора update.Error. func errorsAs(err error, target **Error) bool { type causer interface{ Unwrap() error } for err != nil { if e, ok := err.(*Error); ok { *target = e return true } c, ok := err.(causer) if !ok { return false } err = c.Unwrap() } return false }