github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/updater/updater_test.go (about)

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package updater
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"time"
    11  
    12  	"net/http/httptest"
    13  	"os"
    14  	"path/filepath"
    15  	"runtime"
    16  	"testing"
    17  
    18  	"github.com/keybase/client/go/updater/saltpack"
    19  	"github.com/keybase/client/go/updater/util"
    20  	"github.com/keybase/go-logging"
    21  	"github.com/stretchr/testify/assert"
    22  	"github.com/stretchr/testify/require"
    23  )
    24  
    25  var testLog = &logging.Logger{Module: "test"}
    26  
    27  var testZipPath string
    28  
    29  var testAppStatePath = filepath.Join(os.TempDir(), "KBTest_app_state.json")
    30  
    31  const (
    32  	// shasum -a 256 test/test.zip
    33  	validDigest = "54970995e4d02da631e0634162ef66e2663e0eee7d018e816ac48ed6f7811c84"
    34  	// keybase sign -d -i test.zip
    35  	validSignature = `BEGIN KEYBASE SALTPACK DETACHED SIGNATURE.
    36  kXR7VktZdyH7rvq v5weRa8moXPeKBe e2YLT0PnyHzCrVi RbC1J5uJtYgYyLW eGg4qzsWqkb7hcX
    37  GTVc0vsEUVwBCly qhPdOL0mE19kfxg A4fMqpNGNTY0jtO iMpjwwuIyLBxkCC jHzMiJFskzluz2S
    38  otWUI0nTu2vG2Fx Mgeyqm20Ug8j7Bi N. END KEYBASE SALTPACK DETACHED SIGNATURE.`
    39  	invalidDigest    = "74970995e4d02da631e0634162ef66e2663e0eee7d018e816ac48ed6f7811c84"
    40  	invalidSignature = `BEGIN KEYBASE SALTPACK DETACHED SIGNATURE.
    41  	QXR7VktZdyH7rvq v5wcIkPOwDJ1n11 M8RnkLKQGO2f3Bb fzCeMYz4S6oxLAy
    42  	Cco4N255JFzv2PX E6WWdobANV4guJI iEE8XJb6uudCX4x QWZfnamVAaZpXuW
    43  	vdz65rE7oZsLSdW oxMsbBgG9NVpSJy x3CD6LaC9GlZ4IS ofzkHe401mHjr7M M. END
    44  	KEYBASE SALTPACK DETACHED SIGNATURE.`
    45  )
    46  
    47  func makeKeybaseUpdateTempDir(t *testing.T, updater *Updater, testAsset *Asset) (tmpDir string) {
    48  	// This creates a real KebyaseUpdater.[ID] directory in os.TempDir
    49  	// Then we download the test zip to this directory from testServer
    50  	tmpDir, err := util.MakeTempDir("KeybaseUpdater.", 0700)
    51  	require.NoError(t, err)
    52  	err = updater.downloadAsset(testAsset, tmpDir, UpdateOptions{})
    53  	require.NoError(t, err)
    54  	return tmpDir
    55  }
    56  
    57  func init() {
    58  	_, filename, _, _ := runtime.Caller(0)
    59  	testZipPath = filepath.Join(filepath.Dir(filename), "test/test.zip")
    60  }
    61  
    62  func newTestUpdater(t *testing.T) (*Updater, error) {
    63  	return newTestUpdaterWithServer(t, nil, nil, &testConfig{})
    64  }
    65  
    66  func newTestUpdaterWithServer(t *testing.T, testServer *httptest.Server, update *Update, config Config) (*Updater, error) {
    67  	return NewUpdater(testUpdateSource{testServer: testServer, config: config, update: update}, config, testLog), nil
    68  }
    69  
    70  func newTestContext(options UpdateOptions, cfg Config, response *UpdatePromptResponse) *testUpdateUI {
    71  	return &testUpdateUI{options: options, cfg: cfg, response: response}
    72  }
    73  
    74  type testUpdateUI struct {
    75  	options            UpdateOptions
    76  	cfg                Config
    77  	response           *UpdatePromptResponse
    78  	promptErr          error
    79  	verifyErr          error
    80  	beforeApplyErr     error
    81  	afterApplyErr      error
    82  	errReported        error
    83  	actionReported     UpdateAction
    84  	autoUpdateReported bool
    85  	updateReported     *Update
    86  	successReported    bool
    87  	isCheckCommand     bool
    88  }
    89  
    90  func (u testUpdateUI) BeforeUpdatePrompt(_ Update, _ UpdateOptions) error {
    91  	return nil
    92  }
    93  
    94  func (u testUpdateUI) UpdatePrompt(_ Update, _ UpdateOptions, _ UpdatePromptOptions) (*UpdatePromptResponse, error) {
    95  	if u.promptErr != nil {
    96  		return nil, u.promptErr
    97  	}
    98  	return u.response, nil
    99  }
   100  
   101  func (u testUpdateUI) BeforeApply(update Update) error {
   102  	return u.beforeApplyErr
   103  }
   104  
   105  func (u testUpdateUI) Apply(update Update, options UpdateOptions, tmpDir string) error {
   106  	return nil
   107  }
   108  
   109  func (u testUpdateUI) AfterApply(update Update) error {
   110  	return u.afterApplyErr
   111  }
   112  
   113  func (u testUpdateUI) GetUpdateUI() UpdateUI {
   114  	return u
   115  }
   116  
   117  func (u testUpdateUI) Verify(update Update) error {
   118  	if u.verifyErr != nil {
   119  		return u.verifyErr
   120  	}
   121  	var validCodeSigningKIDs = map[string]bool{
   122  		"0120d7539e27e83a9c8caf8701199c6985c0a96801ff7cb69456e9b3a8a8446c66080a": true, // joshblum (saltine)
   123  	}
   124  	return saltpack.VerifyDetachedFileAtPath(update.Asset.LocalPath, update.Asset.Signature, validCodeSigningKIDs, testLog)
   125  }
   126  
   127  func (u *testUpdateUI) ReportError(err error, update *Update, options UpdateOptions) {
   128  	u.errReported = err
   129  }
   130  
   131  func (u *testUpdateUI) ReportAction(actionResponse UpdatePromptResponse, update *Update, options UpdateOptions) {
   132  	u.actionReported = actionResponse.Action
   133  	autoUpdate, _ := u.cfg.GetUpdateAuto()
   134  	u.autoUpdateReported = autoUpdate
   135  	u.updateReported = update
   136  }
   137  
   138  func (u *testUpdateUI) ReportSuccess(update *Update, options UpdateOptions) {
   139  	u.successReported = true
   140  	u.updateReported = update
   141  }
   142  
   143  func (u *testUpdateUI) AfterUpdateCheck(update *Update) {}
   144  
   145  func (u testUpdateUI) UpdateOptions() UpdateOptions {
   146  	return u.options
   147  }
   148  
   149  func (u testUpdateUI) GetAppStatePath() string {
   150  	return testAppStatePath
   151  }
   152  
   153  func (u testUpdateUI) IsCheckCommand() bool {
   154  	return u.isCheckCommand
   155  }
   156  
   157  func (u testUpdateUI) DeepClean() {}
   158  
   159  type testUpdateSource struct {
   160  	testServer *httptest.Server
   161  	config     Config
   162  	update     *Update
   163  	findErr    error
   164  }
   165  
   166  func (u testUpdateSource) Description() string {
   167  	return "Test"
   168  }
   169  
   170  func testUpdate(uri string) *Update {
   171  	return newTestUpdate(uri, true)
   172  }
   173  
   174  func newTestUpdate(uri string, needUpdate bool) *Update {
   175  	update := &Update{
   176  		Version:     "1.0.1",
   177  		Name:        "Test",
   178  		Description: "Bug fixes",
   179  		InstallID:   "deadbeef",
   180  		RequestID:   "cafedead",
   181  		NeedUpdate:  needUpdate,
   182  	}
   183  	if uri != "" {
   184  		update.Asset = &Asset{
   185  			Name:      "test.zip",
   186  			URL:       uri,
   187  			Digest:    validDigest,
   188  			Signature: validSignature,
   189  		}
   190  	}
   191  	return update
   192  }
   193  
   194  func (u testUpdateSource) FindUpdate(options UpdateOptions) (*Update, error) {
   195  	return u.update, u.findErr
   196  }
   197  
   198  type testConfig struct {
   199  	auto         bool
   200  	autoSet      bool
   201  	autoOverride bool
   202  	installID    string
   203  	err          error
   204  }
   205  
   206  func (c testConfig) GetUpdateAuto() (bool, bool) {
   207  	return c.auto, c.autoSet
   208  }
   209  
   210  func (c *testConfig) SetUpdateAuto(b bool) error {
   211  	c.auto = b
   212  	c.autoSet = true
   213  	return c.err
   214  }
   215  
   216  func (c *testConfig) IsLastUpdateCheckTimeRecent(d time.Duration) bool {
   217  	return true
   218  }
   219  
   220  func (c *testConfig) SetLastUpdateCheckTime() {
   221  
   222  }
   223  
   224  // For overriding the current Auto setting
   225  func (c testConfig) GetUpdateAutoOverride() bool {
   226  	return c.autoOverride
   227  }
   228  
   229  func (c *testConfig) SetUpdateAutoOverride(auto bool) error {
   230  	c.autoOverride = auto
   231  	return nil
   232  }
   233  
   234  func (c testConfig) GetInstallID() string {
   235  	return c.installID
   236  }
   237  
   238  func (c *testConfig) SetInstallID(s string) error {
   239  	c.installID = s
   240  	return c.err
   241  }
   242  
   243  func (c testConfig) GetLastAppliedVersion() string {
   244  	return ""
   245  }
   246  
   247  func (c *testConfig) SetLastAppliedVersion(version string) error {
   248  	return nil
   249  }
   250  
   251  func newDefaultTestUpdateOptions() UpdateOptions {
   252  	return UpdateOptions{
   253  		Version:         "1.0.0",
   254  		Platform:        runtime.GOOS,
   255  		DestinationPath: filepath.Join(os.TempDir(), "Test"),
   256  	}
   257  }
   258  
   259  func testServerForUpdateFile(t *testing.T, path string) *httptest.Server {
   260  	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   261  		f, err := os.Open(path)
   262  		require.NoError(t, err)
   263  		w.Header().Set("Content-Type", "application/zip")
   264  		_, err = io.Copy(w, f)
   265  		require.NoError(t, err)
   266  	}))
   267  }
   268  
   269  func testServerForError(t *testing.T, err error) *httptest.Server {
   270  	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   271  		http.Error(w, err.Error(), 500)
   272  	}))
   273  }
   274  
   275  func testServerNotFound(t *testing.T) *httptest.Server {
   276  	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   277  		http.Error(w, "Not Found", 404)
   278  	}))
   279  }
   280  
   281  func TestUpdaterApply(t *testing.T) {
   282  	testServer := testServerForUpdateFile(t, testZipPath)
   283  	defer testServer.Close()
   284  
   285  	upr, err := newTestUpdaterWithServer(t, testServer, testUpdate(testServer.URL), &testConfig{})
   286  	assert.NoError(t, err)
   287  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, &UpdatePromptResponse{Action: UpdateActionApply, AutoUpdate: true})
   288  	update, err := upr.Update(ctx)
   289  	require.NoError(t, err)
   290  	require.NotNil(t, update)
   291  	t.Logf("Update: %#v\n", *update)
   292  	require.NotNil(t, update.Asset)
   293  	t.Logf("Asset: %#v\n", *update.Asset)
   294  
   295  	auto, autoSet := upr.config.GetUpdateAuto()
   296  	assert.True(t, auto)
   297  	assert.True(t, autoSet)
   298  	assert.Equal(t, "deadbeef", upr.config.GetInstallID())
   299  
   300  	assert.Nil(t, ctx.errReported)
   301  	assert.Equal(t, ctx.actionReported, UpdateActionApply)
   302  	assert.True(t, ctx.autoUpdateReported)
   303  
   304  	require.NotNil(t, ctx.updateReported)
   305  	assert.Equal(t, "deadbeef", ctx.updateReported.InstallID)
   306  	assert.Equal(t, "cafedead", ctx.updateReported.RequestID)
   307  	assert.True(t, ctx.successReported)
   308  
   309  	assert.Equal(t, "apply", UpdateActionApply.String())
   310  }
   311  
   312  func TestUpdaterDownloadError(t *testing.T) {
   313  	testServer := testServerForError(t, fmt.Errorf("bad response"))
   314  	defer testServer.Close()
   315  
   316  	upr, err := newTestUpdaterWithServer(t, testServer, testUpdate(testServer.URL), &testConfig{})
   317  	assert.NoError(t, err)
   318  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, &UpdatePromptResponse{Action: UpdateActionApply, AutoUpdate: true})
   319  	_, err = upr.Update(ctx)
   320  	assert.EqualError(t, err, "Update Error (download): Responded with 500 Internal Server Error")
   321  
   322  	require.NotNil(t, ctx.errReported)
   323  	assert.Equal(t, ctx.errReported.(Error).errorType, DownloadError)
   324  	assert.False(t, ctx.successReported)
   325  }
   326  
   327  func TestUpdaterCancel(t *testing.T) {
   328  	testServer := testServerForUpdateFile(t, testZipPath)
   329  	defer testServer.Close()
   330  
   331  	upr, err := newTestUpdaterWithServer(t, testServer, testUpdate(testServer.URL), &testConfig{})
   332  	assert.NoError(t, err)
   333  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, &UpdatePromptResponse{Action: UpdateActionCancel, AutoUpdate: true})
   334  	_, err = upr.Update(ctx)
   335  	assert.EqualError(t, err, "Update Error (cancel): Canceled")
   336  
   337  	// Don't report error on user cancel
   338  	assert.NoError(t, ctx.errReported)
   339  }
   340  
   341  func TestUpdaterSnooze(t *testing.T) {
   342  	testServer := testServerForUpdateFile(t, testZipPath)
   343  	defer testServer.Close()
   344  
   345  	upr, err := newTestUpdaterWithServer(t, testServer, testUpdate(testServer.URL), &testConfig{})
   346  	assert.NoError(t, err)
   347  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, &UpdatePromptResponse{Action: UpdateActionSnooze, AutoUpdate: true})
   348  	_, err = upr.Update(ctx)
   349  	assert.EqualError(t, err, "Update Error (cancel): Snoozed update")
   350  
   351  	// Don't report error on user snooze
   352  	assert.NoError(t, ctx.errReported)
   353  }
   354  
   355  func TestUpdaterContinue(t *testing.T) {
   356  	testServer := testServerForUpdateFile(t, testZipPath)
   357  	defer testServer.Close()
   358  
   359  	upr, err := newTestUpdaterWithServer(t, testServer, testUpdate(testServer.URL), &testConfig{})
   360  	assert.NoError(t, err)
   361  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, &UpdatePromptResponse{Action: UpdateActionContinue})
   362  	update, err := upr.Update(ctx)
   363  	require.NoError(t, err)
   364  	require.NotNil(t, update)
   365  	require.NotNil(t, update.Asset)
   366  
   367  	auto, autoSet := upr.config.GetUpdateAuto()
   368  	assert.False(t, auto)
   369  	assert.False(t, autoSet)
   370  	assert.Equal(t, "deadbeef", upr.config.GetInstallID())
   371  
   372  	assert.Nil(t, ctx.errReported)
   373  	assert.Empty(t, string(ctx.actionReported))
   374  	assert.False(t, ctx.autoUpdateReported)
   375  
   376  	require.NotNil(t, ctx.updateReported)
   377  	assert.Equal(t, "deadbeef", ctx.updateReported.InstallID)
   378  	assert.Equal(t, "cafedead", ctx.updateReported.RequestID)
   379  	assert.True(t, ctx.successReported)
   380  }
   381  
   382  func TestUpdateNoResponse(t *testing.T) {
   383  	testServer := testServerForUpdateFile(t, testZipPath)
   384  	defer testServer.Close()
   385  
   386  	upr, err := newTestUpdaterWithServer(t, testServer, testUpdate(testServer.URL), &testConfig{})
   387  	assert.NoError(t, err)
   388  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, nil)
   389  	_, err = upr.Update(ctx)
   390  	assert.EqualError(t, err, "Update Error (prompt): No response")
   391  
   392  	require.NotNil(t, ctx.errReported)
   393  	assert.Equal(t, ctx.errReported.(Error).errorType, PromptError)
   394  	assert.False(t, ctx.successReported)
   395  }
   396  
   397  func TestUpdateNoAsset(t *testing.T) {
   398  	testServer := testServerNotFound(t)
   399  	defer testServer.Close()
   400  
   401  	upr, err := newTestUpdaterWithServer(t, testServer, testUpdate(""), &testConfig{})
   402  	assert.NoError(t, err)
   403  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, &UpdatePromptResponse{Action: UpdateActionApply, AutoUpdate: true})
   404  	update, err := upr.Update(ctx)
   405  	assert.NoError(t, err)
   406  	assert.Nil(t, update.Asset)
   407  }
   408  
   409  func testUpdaterError(t *testing.T, errorType ErrorType) {
   410  	testServer := testServerForUpdateFile(t, testZipPath)
   411  	defer testServer.Close()
   412  
   413  	upr, _ := newTestUpdaterWithServer(t, testServer, testUpdate(testServer.URL), &testConfig{})
   414  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, &UpdatePromptResponse{Action: UpdateActionApply, AutoUpdate: true})
   415  	testErr := fmt.Errorf("Test error")
   416  	switch errorType {
   417  	case PromptError:
   418  		ctx.promptErr = testErr
   419  	case VerifyError:
   420  		ctx.verifyErr = testErr
   421  	}
   422  
   423  	_, err := upr.Update(ctx)
   424  	assert.EqualError(t, err, fmt.Sprintf("Update Error (%s): Test error", errorType.String()))
   425  
   426  	require.NotNil(t, ctx.errReported)
   427  	assert.Equal(t, ctx.errReported.(Error).errorType, errorType)
   428  }
   429  
   430  func TestUpdaterErrors(t *testing.T) {
   431  	testUpdaterError(t, PromptError)
   432  	testUpdaterError(t, VerifyError)
   433  }
   434  
   435  func TestUpdaterConfigError(t *testing.T) {
   436  	testServer := testServerForUpdateFile(t, testZipPath)
   437  	defer testServer.Close()
   438  
   439  	upr, _ := newTestUpdaterWithServer(t, testServer, testUpdate(testServer.URL), &testConfig{err: fmt.Errorf("Test config error")})
   440  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, &UpdatePromptResponse{Action: UpdateActionApply, AutoUpdate: true})
   441  
   442  	_, err := upr.Update(ctx)
   443  	assert.NoError(t, err)
   444  
   445  	require.NotNil(t, ctx.errReported)
   446  	assert.Equal(t, ConfigError, ctx.errReported.(Error).errorType)
   447  }
   448  
   449  func TestUpdaterAuto(t *testing.T) {
   450  	testServer := testServerForUpdateFile(t, testZipPath)
   451  	defer testServer.Close()
   452  
   453  	upr, _ := newTestUpdaterWithServer(t, testServer, testUpdate(testServer.URL), &testConfig{auto: true, autoSet: true})
   454  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, &UpdatePromptResponse{Action: UpdateActionApply, AutoUpdate: true})
   455  
   456  	_, err := upr.Update(ctx)
   457  	assert.NoError(t, err)
   458  	assert.Equal(t, UpdateActionAuto, ctx.actionReported)
   459  }
   460  
   461  func TestUpdaterDownloadNil(t *testing.T) {
   462  	upr, err := newTestUpdater(t)
   463  	require.NoError(t, err)
   464  	tmpDir, err := util.MakeTempDir("TestUpdaterDownloadNil", 0700)
   465  	defer util.RemoveFileAtPath(tmpDir)
   466  	require.NoError(t, err)
   467  	err = upr.downloadAsset(nil, tmpDir, UpdateOptions{})
   468  	assert.EqualError(t, err, "No asset to download")
   469  }
   470  
   471  func TestUpdaterApplyError(t *testing.T) {
   472  	testServer := testServerForUpdateFile(t, testZipPath)
   473  	defer testServer.Close()
   474  
   475  	upr, _ := newTestUpdaterWithServer(t, testServer, testUpdate(testServer.URL), &testConfig{auto: true, autoSet: true})
   476  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, &UpdatePromptResponse{Action: UpdateActionApply, AutoUpdate: true})
   477  
   478  	ctx.beforeApplyErr = fmt.Errorf("Test before error")
   479  	_, err := upr.Update(ctx)
   480  	assert.EqualError(t, err, "Update Error (apply): Test before error")
   481  	ctx.beforeApplyErr = nil
   482  
   483  	ctx.afterApplyErr = fmt.Errorf("Test after error")
   484  	_, err = upr.Update(ctx)
   485  	assert.EqualError(t, err, "Update Error (apply): Test after error")
   486  }
   487  
   488  func TestUpdaterNotNeeded(t *testing.T) {
   489  	testServer := testServerForUpdateFile(t, testZipPath)
   490  	defer testServer.Close()
   491  
   492  	upr, err := newTestUpdaterWithServer(t, testServer, newTestUpdate(testServer.URL, false), &testConfig{})
   493  	assert.NoError(t, err)
   494  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, &UpdatePromptResponse{Action: UpdateActionSnooze, AutoUpdate: true})
   495  	update, err := upr.Update(ctx)
   496  	assert.NoError(t, err)
   497  	assert.Nil(t, update)
   498  
   499  	assert.False(t, ctx.successReported)
   500  	assert.Equal(t, "deadbeef", upr.config.GetInstallID())
   501  }
   502  
   503  func TestUpdaterCheckAndUpdate(t *testing.T) {
   504  	if runtime.GOOS == "windows" {
   505  		t.Skip("Skipping on windows")
   506  	}
   507  	testServer := testServerForUpdateFile(t, testZipPath)
   508  	defer testServer.Close()
   509  
   510  	testUpdate := newTestUpdate(testServer.URL, false)
   511  	upr, err := newTestUpdaterWithServer(t, testServer, testUpdate, &testConfig{})
   512  	assert.NoError(t, err)
   513  	defer func() {
   514  		err = upr.CleanupPreviousUpdates()
   515  		assert.NoError(t, err)
   516  	}()
   517  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, &UpdatePromptResponse{Action: UpdateActionSnooze, AutoUpdate: true})
   518  
   519  	// 1.No update from the server
   520  	// Need update = false
   521  	// FindDownloadedAsset = false
   522  	// return updateAvailable = false, updateWasDownloaded = false
   523  	t.Logf("No update from the server")
   524  	updateAvailable, updateWasDownloaded, err := upr.CheckAndDownload(ctx)
   525  	assert.NoError(t, err)
   526  	assert.False(t, updateAvailable)
   527  	assert.False(t, updateWasDownloaded)
   528  	assert.False(t, ctx.successReported)
   529  	assert.Equal(t, "deadbeef", upr.config.GetInstallID())
   530  
   531  	// 2. Download asset from URL
   532  	// Need update = true
   533  	t.Logf("Download asset from URL")
   534  	testUpdate.NeedUpdate = true
   535  	updateAvailable, updateWasDownloaded, err = upr.CheckAndDownload(ctx)
   536  	assert.NoError(t, err)
   537  	assert.True(t, updateAvailable)
   538  	assert.True(t, updateWasDownloaded)
   539  	assert.False(t, ctx.successReported)
   540  	assert.Equal(t, "deadbeef", upr.config.GetInstallID())
   541  
   542  	// 3.Find existing downloaded asset
   543  	// Need update = true
   544  	// FindDownloadedAsset = true
   545  	// return updateAvailable = true, updateWasDownloaded = true
   546  	t.Logf("Find existing downloaded assert")
   547  	tmpDir := makeKeybaseUpdateTempDir(t, upr, testUpdate.Asset)
   548  	updateAvailable, updateWasDownloaded, err = upr.CheckAndDownload(ctx)
   549  	assert.NoError(t, err)
   550  	assert.True(t, updateAvailable)
   551  	assert.False(t, updateWasDownloaded)
   552  	assert.False(t, ctx.successReported)
   553  	assert.Equal(t, "deadbeef", upr.config.GetInstallID())
   554  
   555  	// Run it again to ensure we don't accidentally download again
   556  	t.Logf("Find existing downloaded assert (again)")
   557  	updateAvailable, updateWasDownloaded, err = upr.CheckAndDownload(ctx)
   558  	assert.NoError(t, err)
   559  	assert.True(t, updateAvailable)
   560  	assert.False(t, updateWasDownloaded)
   561  	assert.False(t, ctx.successReported)
   562  	assert.Equal(t, "deadbeef", upr.config.GetInstallID())
   563  
   564  	util.RemoveFileAtPath(tmpDir)
   565  
   566  	// 4.Verify fails b.c. bit flip
   567  	// Need update = true
   568  	// FindDownloadedAsset = true
   569  	// return updateAvailable = false, updateWasDownloaded = false
   570  	t.Logf("bit flip failure verify sig")
   571  	tmpDir = makeKeybaseUpdateTempDir(t, upr, testUpdate.Asset)
   572  	testUpdate.Asset.Signature = invalidSignature
   573  
   574  	updateAvailable, updateWasDownloaded, err = upr.CheckAndDownload(ctx)
   575  	assert.EqualError(t, err, "Update Error (verify): error verifying signature: failed to read header bytes")
   576  	assert.False(t, updateAvailable)
   577  	assert.False(t, updateWasDownloaded)
   578  	assert.False(t, ctx.successReported)
   579  	assert.Equal(t, "deadbeef", upr.config.GetInstallID())
   580  
   581  	util.RemoveFileAtPath(tmpDir)
   582  	testUpdate.Asset.Signature = validSignature
   583  
   584  	// 5.Digest fails b.c. bit flip
   585  	// Need update = true
   586  	// FindDownloadedAsset = true
   587  	// return updateAvailable = false, updateWasDownloaded = false
   588  	t.Logf("bit flip failure verify digest")
   589  	tmpDir = makeKeybaseUpdateTempDir(t, upr, testUpdate.Asset)
   590  	testUpdate.Asset.Digest = invalidDigest
   591  
   592  	updateAvailable, updateWasDownloaded, err = upr.CheckAndDownload(ctx)
   593  	assert.EqualError(t, err, fmt.Sprintf("Update Error (verify): Invalid digest: 54970995e4d02da631e0634162ef66e2663e0eee7d018e816ac48ed6f7811c84 != 74970995e4d02da631e0634162ef66e2663e0eee7d018e816ac48ed6f7811c84 (%s)", filepath.Join(tmpDir, testUpdate.Asset.Name)))
   594  	assert.False(t, updateAvailable)
   595  	assert.False(t, updateWasDownloaded)
   596  	assert.False(t, ctx.successReported)
   597  	assert.Equal(t, "deadbeef", upr.config.GetInstallID())
   598  
   599  	util.RemoveFileAtPath(tmpDir)
   600  	testUpdate.Asset.Digest = validDigest
   601  }
   602  
   603  func TestApplyDownloaded(t *testing.T) {
   604  	if runtime.GOOS == "windows" {
   605  		t.Skip("Skipping on windows")
   606  	}
   607  	testServer := testServerForUpdateFile(t, testZipPath)
   608  	defer testServer.Close()
   609  
   610  	testUpdate := newTestUpdate(testServer.URL, false)
   611  	testAsset := *testUpdate.Asset
   612  	upr, err := newTestUpdaterWithServer(t, testServer, testUpdate, &testConfig{})
   613  	assert.NoError(t, err)
   614  	defer func() {
   615  		err = upr.CleanupPreviousUpdates()
   616  		assert.NoError(t, err)
   617  	}()
   618  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, &UpdatePromptResponse{Action: UpdateActionSnooze, AutoUpdate: true})
   619  	resetCtxErr := func() {
   620  		ctx.promptErr = nil
   621  		ctx.verifyErr = nil
   622  		ctx.beforeApplyErr = nil
   623  		ctx.afterApplyErr = nil
   624  		ctx.errReported = nil
   625  	}
   626  
   627  	// 1. NeedUpdate = false -> return nil
   628  	applied, err := upr.ApplyDownloaded(ctx)
   629  	assert.EqualError(t, err, "No previously downloaded update to apply since client is update to date")
   630  	assert.False(t, applied)
   631  	assert.NotNil(t, ctx.errReported)
   632  	assert.Nil(t, ctx.updateReported)
   633  	assert.False(t, ctx.successReported)
   634  
   635  	resetCtxErr()
   636  
   637  	// 2. Update missing asset
   638  	testUpdate.NeedUpdate = true
   639  	testUpdate.Asset = nil
   640  
   641  	applied, err = upr.ApplyDownloaded(ctx)
   642  	assert.EqualError(t, err, "Update contained no asset to apply. Update version: 1.0.1")
   643  	assert.False(t, applied)
   644  	assert.NotNil(t, ctx.errReported)
   645  	assert.Nil(t, ctx.updateReported)
   646  	assert.False(t, ctx.successReported)
   647  
   648  	resetCtxErr()
   649  	testUpdate.Asset = &testAsset
   650  	tempURL := testUpdate.Asset.URL
   651  	testUpdate.Asset.URL = ""
   652  
   653  	applied, err = upr.ApplyDownloaded(ctx)
   654  	assert.EqualError(t, err, "Update contained no asset to apply. Update version: 1.0.1")
   655  	assert.False(t, applied)
   656  	assert.NotNil(t, ctx.errReported)
   657  	assert.Nil(t, ctx.updateReported)
   658  	assert.False(t, ctx.successReported)
   659  
   660  	resetCtxErr()
   661  	testUpdate.Asset.URL = tempURL
   662  
   663  	// 3. FindDownloadedAsset = false -> return nil
   664  	applied, err = upr.ApplyDownloaded(ctx)
   665  	assert.EqualError(t, err, "No downloaded asset found for version: 1.0.1")
   666  	assert.False(t, applied)
   667  	assert.NotNil(t, ctx.errReported)
   668  	assert.Nil(t, ctx.updateReported)
   669  	assert.False(t, ctx.successReported)
   670  
   671  	resetCtxErr()
   672  
   673  	// 4. FindDownloadedAsset = true -> digest fails
   674  	tmpDir := makeKeybaseUpdateTempDir(t, upr, testUpdate.Asset)
   675  	testUpdate.Asset.Digest = invalidDigest
   676  
   677  	applied, err = upr.ApplyDownloaded(ctx)
   678  	assert.EqualError(t, err, fmt.Sprintf("Update Error (verify): Invalid digest: 54970995e4d02da631e0634162ef66e2663e0eee7d018e816ac48ed6f7811c84 != 74970995e4d02da631e0634162ef66e2663e0eee7d018e816ac48ed6f7811c84 (%s)", filepath.Join(tmpDir, testUpdate.Asset.Name)))
   679  	assert.False(t, applied)
   680  	assert.NotNil(t, ctx.errReported)
   681  	assert.Nil(t, ctx.updateReported)
   682  	assert.False(t, ctx.successReported)
   683  
   684  	resetCtxErr()
   685  	testUpdate.Asset.Digest = validDigest
   686  	util.RemoveFileAtPath(tmpDir)
   687  
   688  	// 5. FindDownloadedAsset = true -> verify fails
   689  	tmpDir = makeKeybaseUpdateTempDir(t, upr, testUpdate.Asset)
   690  	testUpdate.Asset.Signature = invalidSignature
   691  
   692  	applied, err = upr.ApplyDownloaded(ctx)
   693  	assert.EqualError(t, err, "Update Error (verify): error verifying signature: failed to read header bytes")
   694  	assert.False(t, applied)
   695  	assert.NotNil(t, ctx.errReported)
   696  	assert.Nil(t, ctx.updateReported)
   697  	assert.False(t, ctx.successReported)
   698  
   699  	resetCtxErr()
   700  	testUpdate.Asset.Signature = validSignature
   701  	util.RemoveFileAtPath(tmpDir)
   702  
   703  	// 6. FindDownloadedAsset = true -> no error success
   704  	tmpDir = makeKeybaseUpdateTempDir(t, upr, testUpdate.Asset)
   705  
   706  	applied, err = upr.ApplyDownloaded(ctx)
   707  	assert.NoError(t, err)
   708  	assert.True(t, applied)
   709  	assert.Nil(t, ctx.errReported)
   710  	assert.NotNil(t, ctx.updateReported)
   711  	assert.True(t, ctx.successReported)
   712  
   713  	resetCtxErr()
   714  	util.RemoveFileAtPath(tmpDir)
   715  }
   716  
   717  func TestFindDownloadedAsset(t *testing.T) {
   718  	if runtime.GOOS == "windows" {
   719  		t.Skip("Skipping on windows")
   720  	}
   721  	upr, err := newTestUpdater(t)
   722  	assert.NoError(t, err)
   723  	defer func() {
   724  		err = upr.CleanupPreviousUpdates()
   725  		assert.NoError(t, err)
   726  	}()
   727  
   728  	// 1. empty asset
   729  	matchingAssetPath, err := upr.FindDownloadedAsset("")
   730  	assert.EqualError(t, err, "No asset name provided")
   731  	assert.Equal(t, "", matchingAssetPath)
   732  
   733  	// 2. assset given -> did not create KeybaseUpdate.
   734  	matchingAssetPath, err = upr.FindDownloadedAsset("temp")
   735  	assert.NoError(t, err)
   736  	assert.Equal(t, "", matchingAssetPath)
   737  
   738  	// 3. asset given -> created KeybaseUpdate. -> directory empty
   739  	tmpDir, err := util.MakeTempDir("KeybaseUpdater.", 0700)
   740  	assert.NoError(t, err)
   741  	require.NoError(t, err)
   742  
   743  	matchingAssetPath, err = upr.FindDownloadedAsset("temp")
   744  	assert.NoError(t, err)
   745  	assert.Equal(t, "", matchingAssetPath)
   746  
   747  	util.RemoveFileAtPath(tmpDir)
   748  
   749  	// 4. asset given -> created KeybaseUpdate. -> file exists but no match
   750  	tmpDir, err = util.MakeTempDir("KeybaseUpdater.", 0700)
   751  	assert.NoError(t, err)
   752  	tmpFile := filepath.Join(tmpDir, "nottemp")
   753  	err = os.WriteFile(tmpFile, []byte("Contents of temp file"), 0700)
   754  	require.NoError(t, err)
   755  
   756  	matchingAssetPath, err = upr.FindDownloadedAsset("temp")
   757  	assert.NoError(t, err)
   758  	assert.Equal(t, "", matchingAssetPath)
   759  
   760  	util.RemoveFileAtPath(tmpDir)
   761  
   762  	// 5. asset given -> created KeybaseUpdate. -> file exixst and matches
   763  	tmpDir, err = util.MakeTempDir("KeybaseUpdater.", 0700)
   764  	tmpFile = filepath.Join(tmpDir, "temp")
   765  	err = os.WriteFile(tmpFile, []byte("Contents of temp file"), 0700)
   766  	require.NoError(t, err)
   767  
   768  	matchingAssetPath, err = upr.FindDownloadedAsset("temp")
   769  	assert.NoError(t, err)
   770  	assert.Equal(t, tmpFile, matchingAssetPath)
   771  
   772  	util.RemoveFileAtPath(tmpDir)
   773  
   774  }
   775  
   776  func TestUpdaterGuiBusy(t *testing.T) {
   777  	testServer := testServerForUpdateFile(t, testZipPath)
   778  	defer testServer.Close()
   779  
   780  	upr, err := newTestUpdaterWithServer(t, testServer, testUpdate(testServer.URL), &testConfig{auto: true, autoSet: true})
   781  	assert.NoError(t, err)
   782  	ctx := newTestContext(newDefaultTestUpdateOptions(), upr.config, &UpdatePromptResponse{Action: UpdateActionApply, AutoUpdate: true})
   783  	// Expect no error when the app state config is not found, allowing auto update to continue
   784  	_, err = upr.Update(ctx)
   785  	assert.NoError(t, err)
   786  
   787  	// Now put the config file there and make sure the right error is returned
   788  	now := time.Now().Unix() * 1000
   789  	err = os.WriteFile(testAppStatePath, []byte(fmt.Sprintf(`{"isUserActive":true, "changedAtMs":%d}`, now)), 0644)
   790  	assert.NoError(t, err)
   791  	defer util.RemoveFileAtPath(testAppStatePath)
   792  	_, err = upr.Update(ctx)
   793  	assert.EqualError(t, err, "Update Error (guiBusy): User active, retrying later")
   794  
   795  	// If the user was recently active, they are still considered busy.
   796  	err = os.WriteFile(testAppStatePath, []byte(fmt.Sprintf(`{"isUserActive":false, "changedAtMs":%d}`, now)), 0644)
   797  	assert.NoError(t, err)
   798  	_, err = upr.Update(ctx)
   799  	assert.EqualError(t, err, "Update Error (guiBusy): User active, retrying later")
   800  
   801  	// Make sure check command doesn't skip update on active UI
   802  	ctx.isCheckCommand = true
   803  	_, err = upr.Update(ctx)
   804  	assert.NoError(t, err)
   805  
   806  	// If the user wasn't recently active, they are not considered busy
   807  	ctx.isCheckCommand = false
   808  	later := time.Now().Add(-5*time.Minute).Unix() * 1000
   809  	err = os.WriteFile(testAppStatePath, []byte(fmt.Sprintf(`{"isUserActive":false, "changedAtMs":%d}`, later)), 0644)
   810  	assert.NoError(t, err)
   811  	_, err = upr.Update(ctx)
   812  	assert.NoError(t, err)
   813  }