mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 18:00:54 -10:00
490 lines
15 KiB
Go
490 lines
15 KiB
Go
package update
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNormalizeVersionTagAndCompare(t *testing.T) {
|
|
got, err := NormalizeVersionTag("v1.2.3+build.4")
|
|
if err != nil {
|
|
t.Fatalf("NormalizeVersionTag returned error: %v", err)
|
|
}
|
|
if got != "1.2.3" {
|
|
t.Fatalf("NormalizeVersionTag = %q, want 1.2.3", got)
|
|
}
|
|
|
|
comparison, err := CompareSemver("0.2.0", "0.1.9")
|
|
if err != nil {
|
|
t.Fatalf("CompareSemver returned error: %v", err)
|
|
}
|
|
if comparison <= 0 {
|
|
t.Fatal("0.2.0 should be newer than 0.1.9")
|
|
}
|
|
|
|
comparison, err = CompareSemver("v0.1.0", "0.1.0")
|
|
if err != nil {
|
|
t.Fatalf("CompareSemver returned error: %v", err)
|
|
}
|
|
if comparison != 0 {
|
|
t.Fatal("v0.1.0 should match 0.1.0")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeVersionTagAndCompareReportInvalidInput(t *testing.T) {
|
|
if _, err := NormalizeVersionTag("nightly"); err == nil {
|
|
t.Fatal("NormalizeVersionTag should reject invalid versions")
|
|
}
|
|
if _, err := NormalizeVersionTag("v999999999999999999999.0.0"); err == nil {
|
|
t.Fatal("NormalizeVersionTag should reject oversized version components")
|
|
}
|
|
if _, err := CompareSemver("0.2.0", "nightly"); err == nil {
|
|
t.Fatal("CompareSemver should reject invalid versions")
|
|
}
|
|
if _, err := CompareSemver("v999999999999999999999.0.0", "0.1.0"); err == nil {
|
|
t.Fatal("CompareSemver should reject oversized version components")
|
|
}
|
|
}
|
|
|
|
func TestCheckReportsAvailableUpdate(t *testing.T) {
|
|
result, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Fetch: func(_ context.Context, endpoint string) (Release, error) {
|
|
if endpoint != Endpoint(DefaultRepository) {
|
|
t.Fatalf("endpoint = %q, want default", endpoint)
|
|
}
|
|
return releaseForTarget(t, "v0.2.0", "linux", "amd64"), nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Check returned error: %v", err)
|
|
}
|
|
if !result.UpdateAvailable || result.LatestVersion != "0.2.0" {
|
|
t.Fatalf("unexpected update result: %#v", result)
|
|
}
|
|
if !result.ReleaseAsset.Verified || result.ReleaseAsset.ArchiveName != "zero-v0.2.0-linux-x64.tar.gz" || result.ReleaseAsset.ChecksumName != "zero-v0.2.0-linux-x64.tar.gz.sha256" {
|
|
t.Fatalf("unexpected release asset check: %#v", result.ReleaseAsset)
|
|
}
|
|
}
|
|
|
|
func TestResolveTarget(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
name string
|
|
goos string
|
|
goarch string
|
|
platform string
|
|
arch string
|
|
}{
|
|
{input: "linux-x64", name: "linux-x64", goos: "linux", goarch: "amd64", platform: "linux", arch: "x64"},
|
|
{input: " Linux-X64 ", name: "linux-x64", goos: "linux", goarch: "amd64", platform: "linux", arch: "x64"},
|
|
{input: "linux-arm64", name: "linux-arm64", goos: "linux", goarch: "arm64", platform: "linux", arch: "arm64"},
|
|
{input: "macos-x64", name: "macos-x64", goos: "darwin", goarch: "amd64", platform: "macos", arch: "x64"},
|
|
{input: "MacOS-Arm64", name: "macos-arm64", goos: "darwin", goarch: "arm64", platform: "macos", arch: "arm64"},
|
|
{input: "windows-x64", name: "windows-x64", goos: "windows", goarch: "amd64", platform: "windows", arch: "x64"},
|
|
{input: " WINDOWS-ARM64 ", name: "windows-arm64", goos: "windows", goarch: "arm64", platform: "windows", arch: "arm64"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
target, err := ResolveTarget(tt.input)
|
|
if err != nil {
|
|
t.Fatalf("ResolveTarget returned error: %v", err)
|
|
}
|
|
if target.Name != tt.name || target.GOOS != tt.goos || target.GOARCH != tt.goarch || target.Platform != tt.platform || target.Arch != tt.arch {
|
|
t.Fatalf("ResolveTarget(%q) = %#v", tt.input, target)
|
|
}
|
|
})
|
|
}
|
|
|
|
if _, err := ResolveTarget("solaris-sparc"); err == nil || !strings.Contains(err.Error(), "unsupported update target") {
|
|
t.Fatalf("ResolveTarget invalid error = %v, want unsupported target", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckReturnsFetchError(t *testing.T) {
|
|
wantErr := errors.New("network failure")
|
|
|
|
_, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Fetch: func(context.Context, string) (Release, error) {
|
|
return Release{}, wantErr
|
|
},
|
|
})
|
|
|
|
if !errors.Is(err, wantErr) {
|
|
t.Fatalf("Check error = %v, want %v", err, wantErr)
|
|
}
|
|
}
|
|
|
|
func TestCheckRespectsContextCancellation(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
|
defer cancel()
|
|
|
|
_, err := Check(ctx, Options{
|
|
CurrentVersion: "0.1.0",
|
|
Fetch: func(ctx context.Context, _ string) (Release, error) {
|
|
<-ctx.Done()
|
|
return Release{}, ctx.Err()
|
|
},
|
|
})
|
|
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Fatalf("Check error = %v, want context deadline", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckRejectsNegativeTimeout(t *testing.T) {
|
|
_, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Timeout: -time.Second,
|
|
Fetch: func(context.Context, string) (Release, error) {
|
|
t.Fatal("Fetch should not run for invalid timeout")
|
|
return Release{}, nil
|
|
},
|
|
})
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "timeout must be non-negative") {
|
|
t.Fatalf("Check error = %v, want non-negative timeout error", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckRejectsMissingTagName(t *testing.T) {
|
|
_, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Fetch: func(context.Context, string) (Release, error) {
|
|
return Release{HTMLURL: "https://example.test/release"}, nil
|
|
},
|
|
})
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "tag_name") {
|
|
t.Fatalf("Check error = %v, want missing tag_name error", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckRejectsInvalidLatestVersion(t *testing.T) {
|
|
_, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Fetch: func(context.Context, string) (Release, error) {
|
|
return Release{TagName: "nightly"}, nil
|
|
},
|
|
})
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "invalid semantic version") {
|
|
t.Fatalf("Check error = %v, want invalid version error", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckFallsBackReleaseURL(t *testing.T) {
|
|
result, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Repository: "Gitlawb/zero",
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Fetch: func(context.Context, string) (Release, error) {
|
|
release := releaseForTarget(t, "v0.2.0", "linux", "amd64")
|
|
release.HTMLURL = ""
|
|
return release, nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Check returned error: %v", err)
|
|
}
|
|
|
|
wantURL := "https://github.com/Gitlawb/zero/releases/tag/v0.2.0"
|
|
if result.ReleaseURL != wantURL {
|
|
t.Fatalf("ReleaseURL = %q, want %q", result.ReleaseURL, wantURL)
|
|
}
|
|
}
|
|
|
|
func TestCheckFetchesDataEndpoint(t *testing.T) {
|
|
payload := url.QueryEscape(`{"tag_name":"v0.2.0","html_url":"https://example.test/release","assets":[{"name":"zero-v0.2.0-linux-x64.tar.gz","browser_download_url":"https://example.test/zero-v0.2.0-linux-x64.tar.gz"},{"name":"zero-v0.2.0-linux-x64.tar.gz.sha256","browser_download_url":"https://example.test/zero-v0.2.0-linux-x64.tar.gz.sha256"}]}`)
|
|
|
|
result, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Endpoint: "data:application/json," + payload,
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Check returned error: %v", err)
|
|
}
|
|
if !result.UpdateAvailable || result.ReleaseURL != "https://example.test/release" || !result.ReleaseAsset.Verified {
|
|
t.Fatalf("unexpected data endpoint result: %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestCheckResolvesEndpointPrecedence(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
options Options
|
|
env string
|
|
want string
|
|
clearEnv bool
|
|
}{
|
|
{
|
|
name: "endpoint option wins",
|
|
options: Options{Endpoint: "Gitlawb/option-zero", Repository: "Gitlawb/repo-zero"},
|
|
env: "Gitlawb/env-zero",
|
|
want: Endpoint("Gitlawb/option-zero"),
|
|
},
|
|
{
|
|
name: "environment wins over repository",
|
|
options: Options{Repository: "Gitlawb/repo-zero"},
|
|
env: "Gitlawb/env-zero",
|
|
want: Endpoint("Gitlawb/env-zero"),
|
|
},
|
|
{
|
|
name: "repository wins over default",
|
|
options: Options{Repository: "Gitlawb/repo-zero"},
|
|
want: Endpoint("Gitlawb/repo-zero"),
|
|
clearEnv: true,
|
|
},
|
|
{
|
|
name: "default repository last",
|
|
want: Endpoint(DefaultRepository),
|
|
clearEnv: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.clearEnv {
|
|
t.Setenv("ZERO_UPDATE_RELEASE_URL", "")
|
|
} else {
|
|
t.Setenv("ZERO_UPDATE_RELEASE_URL", tt.env)
|
|
}
|
|
options := tt.options
|
|
options.CurrentVersion = "0.1.0"
|
|
options.GOOS = "linux"
|
|
options.GOARCH = "amd64"
|
|
options.Fetch = func(_ context.Context, endpoint string) (Release, error) {
|
|
if endpoint != tt.want {
|
|
t.Fatalf("endpoint = %q, want %q", endpoint, tt.want)
|
|
}
|
|
return releaseForTarget(t, "v0.2.0", "linux", "amd64"), nil
|
|
}
|
|
|
|
if _, err := Check(context.Background(), options); err != nil {
|
|
t.Fatalf("Check returned error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckRejectsMissingReleaseAssets(t *testing.T) {
|
|
_, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Fetch: func(context.Context, string) (Release, error) {
|
|
return Release{
|
|
TagName: "v0.2.0",
|
|
Assets: []Asset{
|
|
{Name: "zero-v0.2.0-linux-arm64.tar.gz"},
|
|
},
|
|
}, nil
|
|
},
|
|
})
|
|
|
|
if err == nil {
|
|
t.Fatal("Check should reject release metadata without expected assets")
|
|
}
|
|
if !strings.Contains(err.Error(), "zero-v0.2.0-linux-x64.tar.gz") || !strings.Contains(err.Error(), "zero-v0.2.0-linux-x64.tar.gz.sha256") {
|
|
t.Fatalf("Check error = %v, want missing archive and checksum names", err)
|
|
}
|
|
}
|
|
|
|
func TestExpectedAssetCheckUsesInstallerArchiveNames(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
version string
|
|
goos string
|
|
goarch string
|
|
archiveName string
|
|
platform string
|
|
arch string
|
|
}{
|
|
{
|
|
name: "linux amd64",
|
|
version: "0.2.0",
|
|
goos: "linux",
|
|
goarch: "amd64",
|
|
archiveName: "zero-v0.2.0-linux-x64.tar.gz",
|
|
platform: "linux",
|
|
arch: "x64",
|
|
},
|
|
{
|
|
name: "macos arm64",
|
|
version: "0.2.0",
|
|
goos: "darwin",
|
|
goarch: "arm64",
|
|
archiveName: "zero-v0.2.0-macos-arm64.tar.gz",
|
|
platform: "macos",
|
|
arch: "arm64",
|
|
},
|
|
{
|
|
name: "windows amd64",
|
|
version: "0.2.0",
|
|
goos: "windows",
|
|
goarch: "amd64",
|
|
archiveName: "zero-v0.2.0-windows-x64.zip",
|
|
platform: "windows",
|
|
arch: "x64",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
check, err := expectedAssetCheck(tt.version, tt.goos, tt.goarch)
|
|
if err != nil {
|
|
t.Fatalf("expectedAssetCheck returned error: %v", err)
|
|
}
|
|
if check.ArchiveName != tt.archiveName || check.ChecksumName != tt.archiveName+".sha256" || check.Platform != tt.platform || check.Arch != tt.arch {
|
|
t.Fatalf("expectedAssetCheck = %#v, want archive %s", check, tt.archiveName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckReportsHTTPError(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "nope", http.StatusInternalServerError)
|
|
}))
|
|
defer server.Close()
|
|
|
|
_, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Endpoint: server.URL,
|
|
})
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "github release check failed") {
|
|
t.Fatalf("Check error = %v, want HTTP status error", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckReportsInvalidHTTPJSON(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte("{"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
_, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Endpoint: server.URL,
|
|
})
|
|
|
|
if err == nil {
|
|
t.Fatal("Check should reject invalid JSON")
|
|
}
|
|
}
|
|
|
|
func TestResolveEndpointAcceptsURLAndRepositorySlug(t *testing.T) {
|
|
got, err := ResolveEndpoint("Gitlawb/alt-zero", DefaultRepository)
|
|
if err != nil {
|
|
t.Fatalf("ResolveEndpoint returned error: %v", err)
|
|
}
|
|
if got != Endpoint("Gitlawb/alt-zero") {
|
|
t.Fatalf("slug endpoint = %q", got)
|
|
}
|
|
|
|
got, err = ResolveEndpoint("https://example.test/latest", DefaultRepository)
|
|
if err != nil {
|
|
t.Fatalf("ResolveEndpoint returned error: %v", err)
|
|
}
|
|
if got != "https://example.test/latest" {
|
|
t.Fatalf("URL endpoint = %q", got)
|
|
}
|
|
|
|
got, err = ResolveEndpoint("", "Gitlawb/fallback")
|
|
if err != nil {
|
|
t.Fatalf("ResolveEndpoint returned error: %v", err)
|
|
}
|
|
if got != Endpoint("Gitlawb/fallback") {
|
|
t.Fatalf("fallback endpoint = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveEndpointRejectsInvalidInput(t *testing.T) {
|
|
_, err := ResolveEndpoint("not a url", DefaultRepository)
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "invalid update endpoint") {
|
|
t.Fatalf("ResolveEndpoint error = %v, want invalid endpoint error", err)
|
|
}
|
|
}
|
|
|
|
func TestFormatResult(t *testing.T) {
|
|
output := Format(Result{
|
|
CurrentVersion: "0.1.0",
|
|
LatestVersion: "0.2.0",
|
|
ReleaseURL: "https://github.com/Gitlawb/zero/releases/tag/v0.2.0",
|
|
TagName: "v0.2.0",
|
|
ReleaseAsset: assetCheckForTest(t, "v0.2.0", "linux", "amd64"),
|
|
UpdateAvailable: true,
|
|
})
|
|
if !strings.Contains(output, "Update available: 0.1.0 -> 0.2.0") {
|
|
t.Fatalf("unexpected update output: %q", output)
|
|
}
|
|
if !strings.Contains(output, "Release asset: zero-v0.2.0-linux-x64.tar.gz") || !strings.Contains(output, "Checksum asset: zero-v0.2.0-linux-x64.tar.gz.sha256") {
|
|
t.Fatalf("update output did not include release assets: %q", output)
|
|
}
|
|
if !strings.Contains(output, "Release target: linux-x64") || !strings.Contains(output, "Download the verified linux-x64 release asset") {
|
|
t.Fatalf("update output did not include target-specific guidance: %q", output)
|
|
}
|
|
if strings.Contains(output, "your platform") {
|
|
t.Fatalf("update output should not use ambiguous platform wording: %q", output)
|
|
}
|
|
|
|
output = Format(Result{
|
|
CurrentVersion: "0.2.0",
|
|
LatestVersion: "0.2.0",
|
|
ReleaseURL: "https://github.com/Gitlawb/zero/releases/tag/v0.2.0",
|
|
TagName: "v0.2.0",
|
|
ReleaseAsset: assetCheckForTest(t, "v0.2.0", "linux", "amd64"),
|
|
UpdateAvailable: false,
|
|
})
|
|
if !strings.Contains(output, "up to date") {
|
|
t.Fatalf("unexpected up-to-date output: %q", output)
|
|
}
|
|
if !strings.Contains(output, "Release target: linux-x64") {
|
|
t.Fatalf("up-to-date output did not include release target: %q", output)
|
|
}
|
|
}
|
|
|
|
func releaseForTarget(t *testing.T, tag string, goos string, goarch string) Release {
|
|
t.Helper()
|
|
check := assetCheckForTest(t, tag, goos, goarch)
|
|
return Release{
|
|
TagName: tag,
|
|
HTMLURL: "https://github.com/Gitlawb/zero/releases/tag/" + tag,
|
|
Assets: []Asset{
|
|
{Name: check.ArchiveName, BrowserDownloadURL: "https://example.test/" + check.ArchiveName},
|
|
{Name: check.ChecksumName, BrowserDownloadURL: "https://example.test/" + check.ChecksumName},
|
|
},
|
|
}
|
|
}
|
|
|
|
func assetCheckForTest(t *testing.T, tag string, goos string, goarch string) AssetCheck {
|
|
t.Helper()
|
|
version, err := NormalizeVersionTag(tag)
|
|
if err != nil {
|
|
t.Fatalf("NormalizeVersionTag(%q): %v", tag, err)
|
|
}
|
|
check, err := expectedAssetCheck(version, goos, goarch)
|
|
if err != nil {
|
|
t.Fatalf("expectedAssetCheck(%q, %q, %q): %v", version, goos, goarch, err)
|
|
}
|
|
check.ArchiveFound = true
|
|
check.ChecksumFound = true
|
|
check.Verified = true
|
|
return check
|
|
}
|