refactor: make it testable

Carlos Alexandro Becker created

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

Change summary

internal/app/app.go            |  2 +-
internal/update/update.go      | 25 ++++++++++++++++++-------
internal/update/update_test.go | 15 +++++++++++++--
3 files changed, 32 insertions(+), 10 deletions(-)

Detailed changes

internal/app/app.go 🔗

@@ -352,7 +352,7 @@ func (app *App) Shutdown() {
 func (app *App) checkForUpdates(ctx context.Context) {
 	checkCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
 	defer cancel()
-	info, err := update.Check(checkCtx)
+	info, err := update.Check(checkCtx, update.Default)
 	if err != nil || !info.Available() {
 		return
 	}

internal/update/update.go 🔗

@@ -17,6 +17,9 @@ const (
 	userAgent    = "crush/1.0"
 )
 
+// Default is the default [Client].
+var Default Client = &github{}
+
 // Info contains information about an available update.
 type Info struct {
 	CurrentVersion string
@@ -24,10 +27,11 @@ type Info struct {
 	ReleaseURL     string
 }
 
+// Available returns true if there's an update available.
 func (i Info) Available() bool { return i.CurrentVersion != i.LatestVersion }
 
 // Check checks if a new version is available.
-func Check(ctx context.Context) (Info, error) {
+func Check(ctx context.Context, client Client) (Info, error) {
 	info := Info{
 		CurrentVersion: version.Version,
 		LatestVersion:  version.Version,
@@ -37,7 +41,7 @@ func Check(ctx context.Context) (Info, error) {
 		return info, nil
 	}
 
-	release, err := fetchLatestRelease(ctx)
+	release, err := client.Latest(ctx)
 	if err != nil {
 		return info, fmt.Errorf("failed to fetch latest release: %w", err)
 	}
@@ -47,14 +51,21 @@ func Check(ctx context.Context) (Info, error) {
 	return info, nil
 }
 
-// githubRelease represents a GitHub release.
-type githubRelease struct {
+// Release represents a GitHub release.
+type Release struct {
 	TagName string `json:"tag_name"`
 	HTMLURL string `json:"html_url"`
 }
 
-// fetchLatestRelease fetches the latest release information from GitHub.
-func fetchLatestRelease(ctx context.Context) (*githubRelease, error) {
+// Client is a client that can get the latest release.
+type Client interface {
+	Latest(ctx context.Context) (*Release, error)
+}
+
+type github struct{}
+
+// Latest implements [Client].
+func (c *github) Latest(ctx context.Context) (*Release, error) {
 	client := &http.Client{
 		Timeout: 30 * time.Second,
 	}
@@ -77,7 +88,7 @@ func fetchLatestRelease(ctx context.Context) (*githubRelease, error) {
 		return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, string(body))
 	}
 
-	var release githubRelease
+	var release Release
 	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
 		return nil, err
 	}

internal/update/update_test.go 🔗

@@ -1,6 +1,7 @@
 package update
 
 import (
+	"context"
 	"testing"
 
 	"github.com/charmbracelet/crush/internal/version"
@@ -14,7 +15,7 @@ func TestCheckForUpdate_DevelopmentVersion(t *testing.T) {
 		version.Version = originalVersion
 	})
 
-	info, err := Check(t.Context())
+	info, err := Check(t.Context(), testClient{})
 	require.NoError(t, err)
 	require.NotNil(t, info)
 	require.False(t, info.Available())
@@ -26,8 +27,18 @@ func TestCheckForUpdate_Old(t *testing.T) {
 	t.Cleanup(func() {
 		version.Version = originalVersion
 	})
-	info, err := Check(t.Context())
+	info, err := Check(t.Context(), testClient{})
 	require.NoError(t, err)
 	require.NotNil(t, info)
 	require.True(t, info.Available())
 }
+
+type testClient struct{}
+
+// Latest implements Client.
+func (t testClient) Latest(ctx context.Context) (*Release, error) {
+	return &Release{
+		TagName: "v0.11.0",
+		HTMLURL: "https://example.org",
+	}, nil
+}