Files
ratatoskr-go/internal/update/update_test.go
Hermes 44ac7d73bb
All checks were successful
CI / test (push) Successful in 47s
CI / build-and-package (amd64, darwin) (push) Successful in 40s
CI / build-and-package (amd64, linux) (push) Successful in 40s
CI / build-and-package (amd64, windows) (push) Successful in 39s
CI / build-and-package (arm64, darwin) (push) Successful in 40s
CI / build-and-package (arm64, linux) (push) Successful in 40s
feat: автообновление бинаря из Gitea Packages (/update, /status, авто-уведомление)
2026-08-16 11:50:23 +05:00

222 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package update
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
// mockGitea поднимает httptest-сервер с бинарём, <filename>.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
}