Initial extension update check testing

First pass at implementing basic test around extension state checking behavior, wanting to discus with team about level of testing to perform and whether this is really the right place.
This commit is contained in:
Andrew Feller 2024-12-01 18:20:55 -05:00
parent 7b91b17395
commit 6bd01d52dd
4 changed files with 150 additions and 4 deletions

View file

@ -43,9 +43,13 @@ func ShouldCheckForExtensionUpdate() bool {
return !IsCI() && IsTerminal(os.Stdout) && IsTerminal(os.Stderr)
}
func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Extension, stateFilePath string) (*ReleaseInfo, error) {
func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Extension, stateFilePath string, now time.Time) (*ReleaseInfo, error) {
if ext.IsLocal() {
return nil, nil
}
stateEntry, _ := getStateEntry(stateFilePath)
if stateEntry != nil && time.Since(stateEntry.CheckedForUpdateAt).Hours() < 24 {
if stateEntry != nil && now.Sub(stateEntry.CheckedForUpdateAt).Hours() < 24 {
return nil, nil
}
@ -54,7 +58,7 @@ func CheckForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Exte
URL: ext.URL(),
}
err := setStateEntry(stateFilePath, time.Now(), *releaseInfo)
err := setStateEntry(stateFilePath, now, *releaseInfo)
if err != nil {
return nil, err
}

View file

@ -6,9 +6,15 @@ import (
"log"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"github.com/cli/cli/v2/pkg/cmd/extension"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestCheckForUpdate(t *testing.T) {
@ -117,6 +123,136 @@ func TestCheckForUpdate(t *testing.T) {
}
}
func TestCheckForExtensionUpdate(t *testing.T) {
now := time.Date(2024, 12, 17, 12, 0, 0, 0, time.UTC)
tests := []struct {
name string
extCurrentVersion string
extLatestVersion string
extIsLocal bool
extURL string
stateEntry *StateEntry
ri ReleaseInfo
wantErr bool
expectedReleaseInfo *ReleaseInfo
expectedStateEntry *StateEntry
}{
{
name: "return latest release given extension is out of date and no state entry",
extCurrentVersion: "v0.1.0",
extLatestVersion: "v1.0.0",
extIsLocal: false,
extURL: "http://example.com",
stateEntry: nil,
expectedReleaseInfo: &ReleaseInfo{
Version: "v1.0.0",
URL: "http://example.com",
},
expectedStateEntry: &StateEntry{
CheckedForUpdateAt: now,
LatestRelease: ReleaseInfo{
Version: "v1.0.0",
URL: "http://example.com",
},
},
},
{
name: "return latest release given extension is out of date and state entry is old enough",
extCurrentVersion: "v0.1.0",
extLatestVersion: "v1.0.0",
extIsLocal: false,
extURL: "http://example.com",
stateEntry: &StateEntry{
CheckedForUpdateAt: now.Add(-24 * time.Hour),
LatestRelease: ReleaseInfo{
Version: "v0.1.0",
URL: "http://example.com",
},
},
expectedReleaseInfo: &ReleaseInfo{
Version: "v1.0.0",
URL: "http://example.com",
},
expectedStateEntry: &StateEntry{
CheckedForUpdateAt: now,
LatestRelease: ReleaseInfo{
Version: "v1.0.0",
URL: "http://example.com",
},
},
},
{
name: "return nothing given extension is out of date but state entry is too recent",
extCurrentVersion: "v0.1.0",
extLatestVersion: "v1.0.0",
extIsLocal: false,
extURL: "http://example.com",
stateEntry: &StateEntry{
CheckedForUpdateAt: now.Add(-23 * time.Hour),
LatestRelease: ReleaseInfo{
Version: "v0.1.0",
URL: "http://example.com",
},
},
expectedReleaseInfo: nil,
expectedStateEntry: &StateEntry{
CheckedForUpdateAt: now.Add(-23 * time.Hour),
LatestRelease: ReleaseInfo{
Version: "v0.1.0",
URL: "http://example.com",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
em := &extensions.ExtensionManagerMock{}
ext := &extensions.ExtensionMock{
CurrentVersionFunc: func() string {
return tt.extCurrentVersion
},
LatestVersionFunc: func() string {
return tt.extLatestVersion
},
IsLocalFunc: func() bool {
return tt.extIsLocal
},
URLFunc: func() string {
return tt.extURL
},
}
// Ensure test is testing actual update available logic
ext.UpdateAvailableFunc = func() bool {
// Should this function be removed from the extension interface?
return extension.UpdateAvailable(ext)
}
// Create state file for test as necessary
stateFilePath := filepath.Join(t.TempDir(), "state.yml")
if tt.stateEntry != nil {
stateEntryYaml, err := yaml.Marshal(tt.stateEntry)
require.NoError(t, err)
require.NoError(t, os.WriteFile(stateFilePath, stateEntryYaml, 0644))
}
actual, err := CheckForExtensionUpdate(em, ext, stateFilePath, now)
if tt.wantErr {
require.Error(t, err)
} else {
require.Equal(t, tt.expectedReleaseInfo, actual)
stateEntry, err := getStateEntry(stateFilePath)
require.NoError(t, err)
require.Equal(t, tt.expectedStateEntry, stateEntry)
}
})
}
}
func tempFilePath() string {
file, err := os.CreateTemp("", "")
if err != nil {

View file

@ -11,6 +11,7 @@ import (
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/extensions"
"gopkg.in/yaml.v3"
)
@ -215,6 +216,10 @@ func (e *Extension) Owner() string {
}
func (e *Extension) UpdateAvailable() bool {
return UpdateAvailable(e)
}
func UpdateAvailable(e extensions.Extension) bool {
if e.IsLocal() ||
e.CurrentVersion() == "" ||
e.LatestVersion() == "" ||

View file

@ -6,6 +6,7 @@ import (
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/update"
@ -83,5 +84,5 @@ func checkForExtensionUpdate(em extensions.ExtensionManager, ext extensions.Exte
}
stateFilePath := filepath.Join(config.StateDir(), "extensions", ext.FullName(), "state.yml")
return update.CheckForExtensionUpdate(em, ext, stateFilePath)
return update.CheckForExtensionUpdate(em, ext, stateFilePath, time.Now())
}