Files
l-ui/internal/selfupdate/selfupdate_test.go
T
Muse e78cb8ccc5 feat(agent): standalone Linux agent binary + agent assets in update chain
New entry cmd/xui-agent links only the agent dependency closure
(internal/agent + config/logger/selfupdate/xray/util leaves), a
drop-in replacement for the dual-mode binary's agent subcommand:
same invocation (xui-agent agent -config ...), same agent.json and
state files, same wire protocol.

selfupdate gains AgentAssetName (x-ui-agent-linux-<arch>); the
agent's one-click update now downloads the agent asset instead of
the panel binary. Pre-split agents keep fetching the panel asset —
the two forms are interchangeable at the install path.
2026-10-23 16:40:35 +00:00

143 lines
4.1 KiB
Go

package selfupdate
import (
"context"
"crypto/sha256"
"encoding/hex"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseSHA256Sidecar(t *testing.T) {
sum := sha256.Sum256([]byte("payload"))
hexSum := hex.EncodeToString(sum[:])
cases := []struct {
in string
want string
ok bool
}{
{hexSum + " x-ui-linux-amd64\n", hexSum, true},
{hexSum + "\n", hexSum, true},
{strings.ToUpper(hexSum) + " *x-ui-linux-amd64", hexSum, true},
{"", "", false},
{"not-a-digest file", "", false},
{hexSum[:40] + " file", "", false},
}
for _, tc := range cases {
got, ok := ParseSHA256Sidecar([]byte(tc.in))
if got != tc.want || ok != tc.ok {
t.Fatalf("ParseSHA256Sidecar(%q) = %q, %v; want %q, %v", tc.in, got, ok, tc.want, tc.ok)
}
}
}
func TestAssetName(t *testing.T) {
if name, err := AssetName("amd64"); err != nil || name != "x-ui-linux-amd64" {
t.Fatalf("amd64: %q, %v", name, err)
}
if name, err := AssetName("arm64"); err != nil || name != "x-ui-linux-arm64" {
t.Fatalf("arm64: %q, %v", name, err)
}
if _, err := AssetName("riscv64"); err == nil {
t.Fatal("unknown arch must error")
}
}
func TestAgentAssetName(t *testing.T) {
if name, err := AgentAssetName("amd64"); err != nil || name != "x-ui-agent-linux-amd64" {
t.Fatalf("amd64: %q, %v", name, err)
}
if name, err := AgentAssetName("arm64"); err != nil || name != "x-ui-agent-linux-arm64" {
t.Fatalf("arm64: %q, %v", name, err)
}
if _, err := AgentAssetName("riscv64"); err == nil {
t.Fatal("unknown arch must error")
}
}
func TestSwapExecutableKeepsBackup(t *testing.T) {
dir := t.TempDir()
exe := filepath.Join(dir, "xui-agent")
if err := os.WriteFile(exe, []byte("old-binary"), 0o755); err != nil {
t.Fatal(err)
}
next := filepath.Join(dir, ".x-ui-update-1")
if err := os.WriteFile(next, []byte("new-binary"), 0o644); err != nil {
t.Fatal(err)
}
bak, err := SwapExecutable(exe, next)
if err != nil {
t.Fatalf("SwapExecutable: %v", err)
}
if bak != exe+".bak" {
t.Fatalf("backup path = %q", bak)
}
got, _ := os.ReadFile(exe)
if string(got) != "new-binary" {
t.Fatalf("exe content = %q, want new-binary", got)
}
old, _ := os.ReadFile(bak)
if string(old) != "old-binary" {
t.Fatalf("bak content = %q, want old-binary", old)
}
info, _ := os.Stat(exe)
if info.Mode().Perm()&0o100 == 0 {
t.Fatal("new binary is not executable")
}
// Rollback restores the old binary.
if err := RollbackExecutable(exe); err != nil {
t.Fatalf("RollbackExecutable: %v", err)
}
got, _ = os.ReadFile(exe)
if string(got) != "old-binary" {
t.Fatalf("after rollback exe = %q", got)
}
}
func TestDownloadVerified(t *testing.T) {
payload := []byte("fake-release-binary")
sum := sha256.Sum256(payload)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasSuffix(r.URL.Path, ".sha256"):
_, _ = w.Write([]byte(hex.EncodeToString(sum[:]) + " x-ui-linux-amd64\n"))
default:
_, _ = w.Write(payload)
}
}))
defer srv.Close()
dest := filepath.Join(t.TempDir(), "dl")
if err := DownloadVerified(context.Background(), srv.Client(), srv.URL+"/x-ui-linux-amd64", srv.URL+"/x-ui-linux-amd64.sha256", dest); err != nil {
t.Fatalf("DownloadVerified: %v", err)
}
got, _ := os.ReadFile(dest)
if string(got) != string(payload) {
t.Fatalf("downloaded content = %q", got)
}
}
func TestDownloadVerifiedRejectsBadChecksum(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, ".sha256") {
_, _ = w.Write([]byte(strings.Repeat("0", 64) + " x-ui-linux-amd64\n"))
return
}
_, _ = w.Write([]byte("tampered"))
}))
defer srv.Close()
dest := filepath.Join(t.TempDir(), "dl")
err := DownloadVerified(context.Background(), srv.Client(), srv.URL+"/a", srv.URL+"/a.sha256", dest)
if err == nil || !strings.Contains(err.Error(), "sha256 mismatch") {
t.Fatalf("want sha256 mismatch error, got %v", err)
}
if _, statErr := os.Stat(dest); !os.IsNotExist(statErr) {
t.Fatal("partial download must be removed on mismatch")
}
}