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
222 lines
6.2 KiB
Go
222 lines
6.2 KiB
Go
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
|
||
} |