Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

475 lines
11 KiB
Go

package update
import (
"archive/tar"
"compress/gzip"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIsDevBuildVersion(t *testing.T) {
tests := []struct {
version string
want bool
}{
{"dev", true},
{"unknown", true},
{"", true},
{"0.1.0", false},
{"v0.1.0", false},
{"0.1.0-2-gabcdef", true},
{"v0.1.0-2-gabcdef-dirty", true},
{"0.1.0-rc1", false},
}
for _, tt := range tests {
t.Run(tt.version, func(t *testing.T) {
assert.Equal(t, tt.want, IsDevBuildVersion(tt.version))
})
}
}
func TestIsNewer(t *testing.T) {
tests := []struct {
v1, v2 string
want bool
}{
{"0.2.0", "0.1.0", true},
{"0.1.0", "0.2.0", false},
{"0.1.0", "0.1.0", false},
{"1.0.0", "0.9.9", true},
{"0.1.0-rc2", "0.1.0-rc1", true},
{"0.1.0", "0.1.0-rc1", true},
}
for _, tt := range tests {
name := tt.v1 + "_vs_" + tt.v2
t.Run(name, func(t *testing.T) {
assert.Equal(t, tt.want, isNewer(tt.v1, tt.v2))
})
}
}
func TestExtractChecksum(t *testing.T) {
body := `abc123 some_other_file.tar.gz
deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef agentsview_0.1.0_linux_amd64.tar.gz
fff000 yet_another.zip`
tests := []struct {
filename string
want string
}{
{"agentsview_0.1.0_linux_amd64.tar.gz", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"},
{"nonexistent.tar.gz", ""},
}
for _, tt := range tests {
t.Run(tt.filename, func(t *testing.T) {
assert.Equal(t, tt.want, extractChecksum(body, tt.filename))
})
}
}
func TestResolveLatestTag(t *testing.T) {
tests := []struct {
name string
status int
location string
wantTag string
wantErrSub string
}{
{
name: "valid 302 redirect",
status: http.StatusFound,
location: "https://github.com/kenn-io/agentsview/releases/tag/v0.30.1",
wantTag: "v0.30.1",
},
{
name: "pre-release tag",
status: http.StatusFound,
location: "https://github.com/kenn-io/agentsview/releases/tag/v0.9.0-rc1",
wantTag: "v0.9.0-rc1",
},
{
name: "200 OK is not a redirect",
status: http.StatusOK,
wantErrSub: "expected redirect",
},
{
name: "redirect target without /tag/",
status: http.StatusFound,
location: "https://github.com/kenn-io/agentsview/releases",
wantErrSub: "unexpected redirect target",
},
{
name: "empty tag after /tag/",
status: http.StatusFound,
location: "https://github.com/kenn-io/agentsview/releases/tag/",
wantErrSub: "empty tag",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, _ *http.Request) {
if tt.location != "" {
w.Header().Set("Location", tt.location)
}
w.WriteHeader(tt.status)
},
))
defer srv.Close()
tag, err := resolveLatestTag(srv.URL)
if tt.wantErrSub != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErrSub)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantTag, tag)
})
}
}
func TestFetchContentLength(t *testing.T) {
tests := []struct {
name string
status int
bodySize int
wantSize int64
wantErrSub string
}{
{
name: "200 with body",
status: http.StatusOK,
bodySize: 1234,
wantSize: 1234,
},
{
name: "404",
status: http.StatusNotFound,
wantErrSub: "404",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, _ *http.Request) {
if tt.bodySize > 0 {
w.Header().Set(
"Content-Length",
fmt.Sprintf("%d", tt.bodySize),
)
}
w.WriteHeader(tt.status)
},
))
defer srv.Close()
size, err := fetchContentLength(srv.URL)
if tt.wantErrSub != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErrSub)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantSize, size)
})
}
}
func TestSanitizePath(t *testing.T) {
destDir := t.TempDir()
tests := []struct {
name string
path string
wantPath string
wantErr bool
}{
{"normal", "agentsview", filepath.Join(destDir, "agentsview"), false},
{"subdir", "dir/agentsview", filepath.Join(destDir, "dir/agentsview"), false},
{"absolute", "/etc/passwd", "", true},
{"traversal", "../../../etc/passwd", "", true},
{"hidden_traversal", "foo/../../etc/passwd", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPath, err := sanitizePath(destDir, tt.path)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantPath, gotPath)
})
}
}
func TestExtractTarGz(t *testing.T) {
srcDir := t.TempDir()
destDir := t.TempDir()
// Create a test tar.gz with a dummy binary
archivePath := filepath.Join(srcDir, "test.tar.gz")
createTestTarGz(t, archivePath, "agentsview", "binary-content")
require.NoError(t, extractTarGz(archivePath, destDir))
content, err := os.ReadFile(
filepath.Join(destDir, "agentsview"),
)
require.NoError(t, err, "read extracted file")
assert.Equal(t, "binary-content", string(content))
}
func TestInstallBinaryToSetsExecutableMode(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Unix mode bits not meaningful on Windows")
}
srcDir := t.TempDir()
dstDir := t.TempDir()
srcPath := filepath.Join(srcDir, "agentsview")
dstPath := filepath.Join(dstDir, "agentsview")
require.NoError(t, os.WriteFile(srcPath, []byte("binary"), 0o644))
require.NoError(t, installBinaryTo(srcPath, dstPath))
info, err := os.Stat(dstPath)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0o755), info.Mode().Perm())
}
func TestInstallBinaryToPreservesOnSourceMissing(t *testing.T) {
dstDir := t.TempDir()
dstPath := filepath.Join(dstDir, "agentsview")
require.NoError(t, os.WriteFile(dstPath, []byte("original"), 0o755))
missingSrc := filepath.Join(t.TempDir(), "does-not-exist")
require.Error(t, installBinaryTo(missingSrc, dstPath), "expected error from missing source")
got, err := os.ReadFile(dstPath)
require.NoError(t, err, "dstPath should still exist")
assert.Equal(t, "original", string(got))
_, err = os.Stat(dstPath + ".new")
assert.True(t, os.IsNotExist(err), "staging .new file should not be left behind")
_, err = os.Stat(dstPath + ".old")
assert.True(t, os.IsNotExist(err), "backup .old file should not be left behind")
}
func TestInstallBinaryToNeverMissingDuringUpdate(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip(
"Windows must rename the running binary aside; " +
"a brief missing window is unavoidable",
)
}
srcDir := t.TempDir()
dstDir := t.TempDir()
srcPath := filepath.Join(srcDir, "agentsview")
dstPath := filepath.Join(dstDir, "agentsview")
require.NoError(t, os.WriteFile(srcPath, []byte("new"), 0o755))
require.NoError(t, os.WriteFile(dstPath, []byte("old"), 0o755))
var observations, missing atomic.Uint64
stop := make(chan struct{})
done := make(chan struct{})
go func() {
defer close(done)
for {
select {
case <-stop:
return
default:
}
if _, err := os.Stat(dstPath); err != nil &&
os.IsNotExist(err) {
missing.Add(1)
}
observations.Add(1)
}
}()
const iterations = 1000
for i := range iterations {
if err := installBinaryTo(srcPath, dstPath); err != nil {
close(stop)
<-done
t.Fatalf("install iteration %d: %v", i, err)
}
}
close(stop)
<-done
t.Logf(
"iterations=%d observations=%d missing=%d",
iterations, observations.Load(), missing.Load(),
)
if observations.Load() < 1000 {
t.Skipf(
"observer ran only %d times, test inconclusive",
observations.Load(),
)
}
assert.Zero(t, missing.Load(),
"dstPath observed missing %d times during install", missing.Load())
}
func TestInstallBinaryToRemovesStaleStagingFile(t *testing.T) {
srcDir := t.TempDir()
dstDir := t.TempDir()
srcPath := filepath.Join(srcDir, "agentsview")
dstPath := filepath.Join(dstDir, "agentsview")
require.NoError(t, os.WriteFile(srcPath, []byte("new-binary"), 0o755))
require.NoError(t, os.WriteFile(dstPath, []byte("old-binary"), 0o755))
stagingPath := dstPath + ".new"
require.NoError(t, os.WriteFile(stagingPath, []byte("stale-staging"), 0o644))
require.NoError(t, installBinaryTo(srcPath, dstPath))
_, err := os.Stat(stagingPath)
assert.True(t, os.IsNotExist(err), "stale staging file should be removed, got err=%v", err)
}
func TestInstallBinaryTo(t *testing.T) {
tests := []struct {
name string
existingDest string
newBinary string
want string
}{
{
name: "Install to empty destination",
existingDest: "",
newBinary: "new-binary",
want: "new-binary",
},
{
name: "Install over existing",
existingDest: "old-binary",
newBinary: "newer-binary",
want: "newer-binary",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
srcDir := t.TempDir()
dstDir := t.TempDir()
srcPath := filepath.Join(srcDir, "agentsview")
dstPath := filepath.Join(dstDir, "agentsview")
if tt.existingDest != "" {
require.NoError(t, os.WriteFile(dstPath, []byte(tt.existingDest), 0o755))
}
require.NoError(t, os.WriteFile(srcPath, []byte(tt.newBinary), 0o755))
require.NoError(t, installBinaryTo(srcPath, dstPath))
got, err := os.ReadFile(dstPath)
require.NoError(t, err)
assert.Equal(t, tt.want, string(got))
if tt.existingDest != "" {
_, err := os.Stat(dstPath + ".old")
assert.True(t, os.IsNotExist(err), "backup .old file should be removed")
}
})
}
}
func TestFormatSize(t *testing.T) {
tests := []struct {
bytes int64
want string
}{
{0, "0 B"},
{500, "500 B"},
{1024, "1.0 KB"},
{1536, "1.5 KB"},
{1048576, "1.0 MB"},
{10485760, "10.0 MB"},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%d_bytes", tt.bytes), func(t *testing.T) {
assert.Equal(t, tt.want, FormatSize(tt.bytes))
})
}
}
func TestCacheRoundtrip(t *testing.T) {
dir := t.TempDir()
saveCache("v1.2.3", dir)
cached, err := loadCache(dir)
require.NoError(t, err)
assert.Equal(t, "v1.2.3", cached.Version)
}
func TestNormalizeSemver(t *testing.T) {
tests := []struct {
input string
want string
}{
{"0.1.0", "v0.1.0"},
{"v0.1.0", "v0.1.0"},
{"0.1.0-rc1", "v0.1.0-rc.1"},
{"0.1.0-2-gabcdef", "v0.1.0"},
{"0.1.0-2-gabcdef-dirty", "v0.1.0"},
{"1.0.0-beta10", "v1.0.0-beta.10"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
assert.Equal(t, tt.want, normalizeSemver(tt.input))
})
}
}
func createTestTarGz(
t *testing.T,
archivePath, fileName, content string,
) {
t.Helper()
f, err := os.Create(archivePath)
require.NoError(t, err)
defer f.Close()
gw := gzip.NewWriter(f)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
data := []byte(content)
header := &tar.Header{
Name: fileName,
Mode: 0o755,
Size: int64(len(data)),
}
require.NoError(t, tw.WriteHeader(header))
_, err = tw.Write(data)
require.NoError(t, err)
}