feat: автообновление бинаря из Gitea Packages (/update, /status, авто-уведомление)
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
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
This commit is contained in:
222
internal/update/update_test.go
Normal file
222
internal/update/update_test.go
Normal file
@@ -0,0 +1,222 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user