mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-27 05:57:27 +00:00
130fc61061
Main is failing Backend tests (rest-1) on
TestIssue2285LastUpdateCheckRecordsEffectiveChannelOutcome. The exact
server archive rule (ab0aa1ef0) offers an update only when the release
carries the archive for the host architecture, and the #2285 test
releases carried no assets, so its update-offered cases could never
report available. The two check tests also hard-coded a linux-amd64
archive, which failed on arm64 hosts.
Build every fixture archive from the same runtime asset helper the
updater uses. Test only.
305 lines
9.8 KiB
Go
305 lines
9.8 KiB
Go
package updates
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
)
|
|
|
|
func newReleaseServer(t *testing.T, releases []ReleaseInfo, hitCount *int32) *httptest.Server {
|
|
t.Helper()
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != updateReleaseAPIPath() {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
if hitCount != nil {
|
|
atomic.AddInt32(hitCount, 1)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(releases)
|
|
}))
|
|
}
|
|
|
|
func TestCheckForUpdatesWithChannel_UsesConfiguredRepoPath(t *testing.T) {
|
|
// Exercise published-release behaviour independently of the checkout identity.
|
|
withBuildVersion(t, "6.4.0")
|
|
var hits int32
|
|
releases := []ReleaseInfo{
|
|
{
|
|
TagName: "v99.0.0",
|
|
Name: "v99.0.0",
|
|
Prerelease: false,
|
|
PublishedAt: time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC),
|
|
},
|
|
}
|
|
|
|
t.Setenv("PULSE_GITHUB_REPO", "example/pulse-fork")
|
|
server := newReleaseServer(t, releases, &hits)
|
|
defer server.Close()
|
|
|
|
t.Setenv("PULSE_UPDATE_SERVER", server.URL)
|
|
|
|
manager := NewManager(&config.Config{UpdateChannel: "stable"})
|
|
if _, err := manager.CheckForUpdatesWithChannel(context.Background(), ""); err != nil {
|
|
t.Fatalf("CheckForUpdatesWithChannel returned error: %v", err)
|
|
}
|
|
if got := atomic.LoadInt32(&hits); got != 1 {
|
|
t.Fatalf("expected 1 request, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestCheckForUpdatesWithChannel_SourceBuild(t *testing.T) {
|
|
withBuildVersion(t, "6.4.0")
|
|
markerPath := "BUILD_FROM_SOURCE"
|
|
if err := os.WriteFile(markerPath, []byte("1"), 0644); err != nil {
|
|
t.Fatalf("write %s: %v", markerPath, err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = os.Remove(markerPath)
|
|
})
|
|
|
|
manager := NewManager(&config.Config{UpdateChannel: "stable"})
|
|
|
|
info, err := manager.CheckForUpdatesWithChannel(context.Background(), "")
|
|
if err != nil {
|
|
t.Fatalf("CheckForUpdatesWithChannel returned error: %v", err)
|
|
}
|
|
if info.Available {
|
|
t.Fatalf("expected no updates for source build, got available")
|
|
}
|
|
if info.LatestVersion != info.CurrentVersion {
|
|
t.Fatalf("LatestVersion = %q, want %q", info.LatestVersion, info.CurrentVersion)
|
|
}
|
|
}
|
|
|
|
func TestCheckForUpdatesWithChannel_DiagnosticBuild(t *testing.T) {
|
|
oldBuildVersion := BuildVersion
|
|
BuildVersion = "6.4.1+test.1913.bd37ae18"
|
|
t.Cleanup(func() { BuildVersion = oldBuildVersion })
|
|
manager := NewManager(&config.Config{UpdateChannel: "stable"})
|
|
info, err := manager.CheckForUpdatesWithChannel(context.Background(), "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Available || info.CurrentVersion != BuildVersion || info.LatestVersion != BuildVersion {
|
|
t.Fatalf("diagnostic build must not enter release update flow: %+v", info)
|
|
}
|
|
}
|
|
|
|
func TestCheckForUpdatesWithChannel_AvailableUsesCache(t *testing.T) {
|
|
// Exercise published-release behaviour independently of the checkout identity.
|
|
withBuildVersion(t, "6.4.0")
|
|
var hits int32
|
|
releaseTime := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
|
|
releases := []ReleaseInfo{
|
|
{
|
|
TagName: "v99.0.0",
|
|
Name: "v99.0.0",
|
|
Body: "Release notes",
|
|
Prerelease: false,
|
|
PublishedAt: releaseTime,
|
|
Assets: []ReleaseAsset{runtimeArchiveFixture(t, "v99.0.0")},
|
|
},
|
|
}
|
|
|
|
server := newReleaseServer(t, releases, &hits)
|
|
defer server.Close()
|
|
|
|
t.Setenv("PULSE_UPDATE_SERVER", server.URL)
|
|
|
|
manager := NewManager(&config.Config{UpdateChannel: "stable"})
|
|
|
|
info, err := manager.CheckForUpdatesWithChannel(context.Background(), "")
|
|
if err != nil {
|
|
t.Fatalf("CheckForUpdatesWithChannel returned error: %v", err)
|
|
}
|
|
if !info.Available {
|
|
t.Fatalf("expected update to be available")
|
|
}
|
|
if info.LatestVersion != "99.0.0" {
|
|
t.Fatalf("LatestVersion = %q, want 99.0.0", info.LatestVersion)
|
|
}
|
|
if info.DownloadURL == "" {
|
|
t.Fatalf("DownloadURL not set")
|
|
}
|
|
|
|
info2, err := manager.CheckForUpdatesWithChannel(context.Background(), "")
|
|
if err != nil {
|
|
t.Fatalf("CheckForUpdatesWithChannel second call error: %v", err)
|
|
}
|
|
if info2.LatestVersion != info.LatestVersion {
|
|
t.Fatalf("cached LatestVersion = %q, want %q", info2.LatestVersion, info.LatestVersion)
|
|
}
|
|
if got := atomic.LoadInt32(&hits); got != 1 {
|
|
t.Fatalf("expected 1 request, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestCheckForUpdatesWithChannel_NoReleases(t *testing.T) {
|
|
// Exercise published-release behaviour independently of the checkout identity.
|
|
withBuildVersion(t, "6.4.0")
|
|
var hits int32
|
|
releases := []ReleaseInfo{
|
|
{
|
|
TagName: "v99.0.0-rc.1",
|
|
Name: "v99.0.0-rc.1",
|
|
Prerelease: true,
|
|
PublishedAt: time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC),
|
|
},
|
|
}
|
|
|
|
server := newReleaseServer(t, releases, &hits)
|
|
defer server.Close()
|
|
|
|
t.Setenv("PULSE_UPDATE_SERVER", server.URL)
|
|
|
|
manager := NewManager(&config.Config{UpdateChannel: "stable"})
|
|
|
|
info, err := manager.CheckForUpdatesWithChannel(context.Background(), "")
|
|
if err != nil {
|
|
t.Fatalf("CheckForUpdatesWithChannel returned error: %v", err)
|
|
}
|
|
if info.Available {
|
|
t.Fatalf("expected no updates for stable channel with only prereleases")
|
|
}
|
|
if got := atomic.LoadInt32(&hits); got != 1 {
|
|
t.Fatalf("expected 1 request, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestCheckForUpdates_Wrapper(t *testing.T) {
|
|
// Exercise published-release behaviour independently of the checkout identity.
|
|
withBuildVersion(t, "6.4.0")
|
|
var hits int32
|
|
releases := []ReleaseInfo{
|
|
{
|
|
TagName: "v99.1.0",
|
|
Name: "v99.1.0",
|
|
Body: "Release notes",
|
|
Prerelease: false,
|
|
PublishedAt: time.Date(2024, 2, 3, 4, 5, 6, 0, time.UTC),
|
|
Assets: []ReleaseAsset{runtimeArchiveFixture(t, "v99.1.0")},
|
|
},
|
|
}
|
|
|
|
server := newReleaseServer(t, releases, &hits)
|
|
defer server.Close()
|
|
|
|
t.Setenv("PULSE_UPDATE_SERVER", server.URL)
|
|
|
|
manager := NewManager(&config.Config{UpdateChannel: "stable"})
|
|
|
|
info, err := manager.CheckForUpdates(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("CheckForUpdates returned error: %v", err)
|
|
}
|
|
if !info.Available {
|
|
t.Fatalf("expected update to be available")
|
|
}
|
|
if info.LatestVersion != "99.1.0" {
|
|
t.Fatalf("LatestVersion = %q, want 99.1.0", info.LatestVersion)
|
|
}
|
|
}
|
|
|
|
func TestForcedUpdateCheckRefreshesSavedChannelCache(t *testing.T) {
|
|
// Exercise published-release behaviour independently of the checkout identity.
|
|
withBuildVersion(t, "6.4.0")
|
|
var hits int32
|
|
server := newReleaseServer(t, []ReleaseInfo{{TagName: "v99.0.0", Assets: []ReleaseAsset{{Name: "pulse-v99.0.0-linux-amd64.tar.gz", BrowserDownloadURL: "https://example.com/new.tar.gz"}}}}, &hits)
|
|
defer server.Close()
|
|
t.Setenv("PULSE_UPDATE_SERVER", server.URL)
|
|
manager := NewManager(&config.Config{UpdateChannel: "stable"})
|
|
manager.checkCache["stable"] = &UpdateInfo{LatestVersion: "98.0.0"}
|
|
manager.cacheTime["stable"] = time.Now()
|
|
cached, err := manager.CheckForUpdates(context.Background())
|
|
if err != nil || cached.LatestVersion != "98.0.0" || hits != 0 {
|
|
t.Fatalf("cached check: %+v %v hits=%d", cached, err, hits)
|
|
}
|
|
fresh, err := manager.CheckForUpdatesWithOptions(context.Background(), UpdateCheckOptions{Force: true})
|
|
if err != nil || fresh.LatestVersion != "99.0.0" {
|
|
t.Fatalf("fresh check: %+v %v", fresh, err)
|
|
}
|
|
again, err := manager.CheckForUpdates(context.Background())
|
|
if err != nil || again.LatestVersion != "99.0.0" || atomic.LoadInt32(&hits) != 1 {
|
|
t.Fatalf("refreshed cache: %+v %v hits=%d", again, err, hits)
|
|
}
|
|
}
|
|
|
|
func TestUpdateInfoReleaseDateOmitsUnknownTime(t *testing.T) {
|
|
for _, date := range []time.Time{{}, time.Date(2026, 9, 10, 14, 6, 1, 0, time.UTC)} {
|
|
raw, err := json.Marshal(UpdateInfo{ReleaseDate: date})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var wire map[string]any
|
|
if err := json.Unmarshal(raw, &wire); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
value, present := wire["releaseDate"]
|
|
if present == date.IsZero() {
|
|
t.Fatalf("unexpected date presence: %s", raw)
|
|
}
|
|
if present && value != date.Format(time.RFC3339) {
|
|
t.Fatalf("date changed: %v", value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestForcedUpdateCheckDoesNotReturnCachedResultOnRateLimit(t *testing.T) {
|
|
setRetrySettingsForTest(t, 1, time.Millisecond, time.Millisecond)
|
|
withBuildVersion(t, "6.4.0")
|
|
t.Setenv("PULSE_UPDATE_SERVER", "")
|
|
original := http.DefaultTransport
|
|
http.DefaultTransport = roundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
|
status := http.StatusNotFound
|
|
body := "not found"
|
|
header := http.Header{}
|
|
if req.URL.Host == "api.github.com" {
|
|
status = http.StatusForbidden
|
|
body = `{"message":"API rate limit exceeded"}`
|
|
header.Set("X-RateLimit-Remaining", "0")
|
|
}
|
|
return &http.Response{StatusCode: status, Body: io.NopCloser(strings.NewReader(body)), Header: header, Request: req}, nil
|
|
})
|
|
t.Cleanup(func() { http.DefaultTransport = original })
|
|
manager := NewManager(&config.Config{UpdateChannel: "stable"})
|
|
old := &UpdateInfo{LatestVersion: "6.4.1"}
|
|
at := time.Now()
|
|
manager.checkCache["stable"] = old
|
|
manager.cacheTime["stable"] = at
|
|
info, err := manager.CheckForUpdatesWithOptions(context.Background(), UpdateCheckOptions{Force: true})
|
|
if err == nil || info != nil {
|
|
t.Fatalf("forced failure returned %+v, %v", info, err)
|
|
}
|
|
if manager.checkCache["stable"] != old || !manager.cacheTime["stable"].Equal(at) {
|
|
t.Fatal("failure replaced cache or freshness")
|
|
}
|
|
if manager.GetStatus().Status != "error" {
|
|
t.Fatalf("status=%+v", manager.GetStatus())
|
|
}
|
|
}
|
|
|
|
// runtimeArchiveFixture returns the server archive the update check requires
|
|
// for the architecture running the test. The check offers an update only when
|
|
// that exact archive exists, so a hard-coded linux-amd64 fixture made these
|
|
// tests fail on arm64 hosts.
|
|
func runtimeArchiveFixture(t *testing.T, tag string) ReleaseAsset {
|
|
t.Helper()
|
|
asset, ok := updateReleaseAssetForRuntime(tag)
|
|
if !ok {
|
|
t.Skipf("no Pulse server archive is published for %s", runtime.GOARCH)
|
|
}
|
|
return ReleaseAsset{Name: asset.Name, BrowserDownloadURL: "https://example.com/" + asset.Name}
|
|
}
|