github.com/creativeprojects/go-selfupdate@v1.2.0/update_test.go (about)

     1  package selfupdate
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/Masterminds/semver/v3"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func TestUpdateCommandWithWrongVersion(t *testing.T) {
    19  	_, err := UpdateCommand(context.Background(), "path", "wrong version", ParseSlug("test/test"))
    20  	assert.Error(t, err)
    21  	assert.ErrorIs(t, err, semver.ErrInvalidSemVer)
    22  }
    23  
    24  func TestUpdateCommand(t *testing.T) {
    25  	current := "0.10.0"
    26  	new := "1.0.0"
    27  	source := mockSourceRepository(t)
    28  	updater, err := NewUpdater(Config{Source: source})
    29  	require.NoError(t, err)
    30  
    31  	filename := setupCurrentVersion(t)
    32  
    33  	rel, err := updater.UpdateCommand(context.Background(), filename, current, ParseSlug("creativeprojects/new_version"))
    34  	require.NoError(t, err)
    35  	assert.Equal(t, new, rel.Version())
    36  
    37  	assertNewVersion(t, filename)
    38  }
    39  
    40  func TestUpdateViaSymlink(t *testing.T) {
    41  	if runtime.GOOS == "windows" {
    42  		t.Skip("skipping because creating symlink on windows requires admin privilege")
    43  	}
    44  
    45  	current := "0.10.0"
    46  	new := "1.0.0"
    47  	source := mockSourceRepository(t)
    48  	updater, err := NewUpdater(Config{Source: source})
    49  	require.NoError(t, err)
    50  
    51  	exePath := setupCurrentVersion(t)
    52  	symPath := exePath + "-sym"
    53  
    54  	err = os.Symlink(exePath, symPath)
    55  	require.NoError(t, err)
    56  
    57  	rel, err := updater.UpdateCommand(context.Background(), symPath, current, ParseSlug("creativeprojects/new_version"))
    58  	require.NoError(t, err)
    59  	assert.Equal(t, new, rel.Version())
    60  
    61  	// check actual file (not symlink)
    62  	assertNewVersion(t, exePath)
    63  
    64  	s, err := os.Lstat(symPath)
    65  	require.NoError(t, err)
    66  	if s.Mode()&os.ModeSymlink == 0 {
    67  		t.Fatalf("%s is not a symlink.", symPath)
    68  	}
    69  	// check symlink
    70  	assertNewVersion(t, symPath)
    71  }
    72  
    73  func TestUpdateBrokenSymlinks(t *testing.T) {
    74  	if runtime.GOOS == "windows" {
    75  		t.Skip("skipping because creating symlink on windows requires admin privilege")
    76  	}
    77  
    78  	updater, err := NewUpdater(Config{Source: mockSourceRepository(t)})
    79  	require.NoError(t, err)
    80  
    81  	// unknown-xxx -> unknown-yyy -> {not existing}
    82  	xxx := "unknown-xxx"
    83  	yyy := "unknown-yyy"
    84  
    85  	err = os.Symlink("not-existing", yyy)
    86  	require.NoError(t, err)
    87  	defer os.Remove(yyy)
    88  
    89  	err = os.Symlink(yyy, xxx)
    90  	require.NoError(t, err)
    91  	defer os.Remove(xxx)
    92  
    93  	for _, filename := range []string{yyy, xxx} {
    94  		_, err := updater.UpdateCommand(context.Background(), filename, "0.10.0", ParseSlug("owner/repo"))
    95  		assert.Error(t, err)
    96  		assert.Contains(t, err.Error(), "failed to resolve symlink")
    97  	}
    98  }
    99  
   100  func TestNotExistingCommandPath(t *testing.T) {
   101  	_, err := UpdateCommand(context.Background(), "not-existing-command-path", "1.2.2", ParseSlug("owner/repo"))
   102  	assert.Error(t, err)
   103  	assert.Contains(t, err.Error(), "file may not exist")
   104  }
   105  
   106  func TestNoReleaseFoundForUpdate(t *testing.T) {
   107  	finalVersion := "1.0.0"
   108  	fake := filepath.FromSlash("./testdata/fake-executable")
   109  	updater, err := NewUpdater(Config{Source: &MockSource{}})
   110  	require.NoError(t, err)
   111  
   112  	rel, err := updater.UpdateCommand(context.Background(), fake, finalVersion, ParseSlug("owner/repo"))
   113  	assert.NoError(t, err)
   114  	assert.Equal(t, finalVersion, rel.Version())
   115  	assert.Empty(t, rel.URL)
   116  	assert.Empty(t, rel.AssetURL)
   117  	assert.Empty(t, rel.ReleaseNotes)
   118  }
   119  
   120  func TestCurrentIsTheLatest(t *testing.T) {
   121  	filename := setupCurrentVersion(t)
   122  
   123  	updater, err := NewUpdater(Config{Source: mockSourceRepository(t)})
   124  	require.NoError(t, err)
   125  
   126  	latest := "1.0.0"
   127  	rel, err := updater.UpdateCommand(context.Background(), filename, latest, ParseSlug("creativeprojects/new_version"))
   128  	assert.NoError(t, err)
   129  	assert.Equal(t, latest, rel.Version())
   130  	assert.NotEmpty(t, rel.URL)
   131  	assert.NotEmpty(t, rel.AssetURL)
   132  	assert.NotEmpty(t, rel.ReleaseNotes)
   133  }
   134  
   135  func TestBrokenBinaryUpdate(t *testing.T) {
   136  	fake := filepath.FromSlash("./testdata/fake-executable")
   137  
   138  	source := NewMockSource([]SourceRelease{
   139  		&GitHubRelease{
   140  			name:        "v2.0.0",
   141  			tagName:     "v2.0.0",
   142  			url:         "v2.0.0",
   143  			publishedAt: time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC),
   144  			assets: []SourceAsset{
   145  				&GitHubAsset{
   146  					id:   1,
   147  					name: "invalid_v2.0.0_linux_amd64.tar.gz",
   148  					url:  "invalid_v2.0.0_linux_amd64.tar.gz",
   149  					size: len("invalid content"),
   150  				},
   151  				&GitHubAsset{
   152  					id:   2,
   153  					name: "invalid_v2.0.0_darwin_amd64.tar.gz",
   154  					url:  "invalid_v2.0.0_darwin_amd64.tar.gz",
   155  					size: len("invalid content"),
   156  				},
   157  				&GitHubAsset{
   158  					id:   3,
   159  					name: "invalid_v2.0.0_darwin_arm64.tar.gz",
   160  					url:  "invalid_v2.0.0_darwin_arm64.tar.gz",
   161  					size: len("invalid content"),
   162  				},
   163  				&GitHubAsset{
   164  					id:   4,
   165  					name: "invalid_v2.0.0_windows_amd64.zip",
   166  					url:  "invalid_v2.0.0_windows_amd64.zip",
   167  					size: len("invalid content"),
   168  				},
   169  			},
   170  		},
   171  	}, map[int64][]byte{
   172  		1: []byte("invalid content"),
   173  		2: []byte("invalid content"),
   174  		3: []byte("invalid content"),
   175  		4: []byte("invalid content"),
   176  	})
   177  
   178  	updater, err := NewUpdater(Config{Source: source})
   179  	require.NoError(t, err)
   180  
   181  	_, err = updater.UpdateCommand(context.Background(), fake, "1.2.2", ParseSlug("rhysd-test/test-incorrect-release"))
   182  	require.Error(t, err)
   183  	assert.Contains(t, err.Error(), "failed to decompress")
   184  }
   185  
   186  func TestInvalidSlugForUpdate(t *testing.T) {
   187  	fake := filepath.FromSlash("./testdata/fake-executable")
   188  	_, err := UpdateCommand(context.Background(), fake, "1.0.0", ParseSlug("rhysd/"))
   189  	assert.Error(t, err)
   190  }
   191  
   192  func TestInvalidAssetURL(t *testing.T) {
   193  	err := UpdateTo(context.Background(), "https://github.com/creativeprojects/non-existing-repo/releases/download/v1.2.3/foo.zip", "foo.zip", "foo")
   194  	assert.Error(t, err)
   195  	assert.Contains(t, err.Error(), "failed to download a release file")
   196  }
   197  
   198  func TestBrokenAsset(t *testing.T) {
   199  	asset := "https://github.com/rhysd-test/test-incorrect-release/releases/download/invalid/broken-zip.zip"
   200  	err := UpdateTo(context.Background(), asset, "broken-zip.zip", "foo")
   201  	assert.Error(t, err)
   202  	assert.Contains(t, err.Error(), "failed to decompress zip file")
   203  }
   204  
   205  func TestBrokenGitHubEnterpriseURL(t *testing.T) {
   206  	source, _ := NewGitHubSource(GitHubConfig{APIToken: "my_token", EnterpriseBaseURL: "https://example.com"})
   207  	up, err := NewUpdater(Config{Source: source})
   208  	assert.NoError(t, err)
   209  
   210  	err = up.UpdateTo(
   211  		context.Background(),
   212  		&Release{AssetURL: "https://example.com",
   213  			repository: NewRepositorySlug("test", "test")},
   214  		"foo")
   215  	assert.Error(t, err)
   216  	assert.Contains(t, err.Error(), "failed to call GitHub Releases API for getting the asset")
   217  }
   218  
   219  // ======================== Test validate with Mock ============================================
   220  
   221  func TestNoValidationFile(t *testing.T) {
   222  	source := &MockSource{}
   223  	release := &Release{
   224  		repository:        NewRepositorySlug("test", "test"),
   225  		ValidationAssetID: 123,
   226  	}
   227  	updater := &Updater{
   228  		source: source,
   229  	}
   230  	err := updater.validate(context.Background(), release, []byte("some data"))
   231  	assert.EqualError(t, err, fmt.Sprintf("failed reading validation data \"\": %s", ErrAssetNotFound.Error()))
   232  	assert.ErrorIs(t, err, ErrAssetNotFound)
   233  }
   234  
   235  func TestValidationWrongHash(t *testing.T) {
   236  	hashData, err := os.ReadFile("testdata/SHA256SUM")
   237  	require.NoError(t, err)
   238  
   239  	source := &MockSource{
   240  		files: map[int64][]byte{
   241  			123: hashData,
   242  		},
   243  	}
   244  	release := &Release{
   245  		repository:        NewRepositorySlug("test", "test"),
   246  		ValidationAssetID: 123,
   247  		AssetName:         "foo.zip",
   248  	}
   249  	updater := &Updater{
   250  		source:    source,
   251  		validator: &ChecksumValidator{},
   252  	}
   253  
   254  	data, err := os.ReadFile("testdata/foo.tar.xz")
   255  	require.NoError(t, err)
   256  
   257  	err = updater.validate(context.Background(), release, data)
   258  	require.Error(t, err)
   259  	assert.True(t, errors.Is(err, ErrChecksumValidationFailed), "Not the error we expected")
   260  }
   261  
   262  func TestValidationReadError(t *testing.T) {
   263  	hashData, err := os.ReadFile("testdata/SHA256SUM")
   264  	require.NoError(t, err)
   265  
   266  	source := &MockSource{
   267  		readError: true,
   268  		files: map[int64][]byte{
   269  			123: hashData,
   270  		},
   271  	}
   272  	release := &Release{
   273  		repository:        NewRepositorySlug("test", "test"),
   274  		ValidationAssetID: 123,
   275  		AssetName:         "foo.tar.xz",
   276  	}
   277  	updater := &Updater{
   278  		source:    source,
   279  		validator: &ChecksumValidator{},
   280  	}
   281  
   282  	data, err := os.ReadFile("testdata/foo.tar.xz")
   283  	require.NoError(t, err)
   284  
   285  	err = updater.validate(context.Background(), release, data)
   286  	require.Error(t, err)
   287  	assert.True(t, errors.Is(err, errTestRead))
   288  }
   289  
   290  func TestValidationSuccess(t *testing.T) {
   291  	hashData, err := os.ReadFile("testdata/SHA256SUM")
   292  	require.NoError(t, err)
   293  
   294  	source := &MockSource{
   295  		files: map[int64][]byte{
   296  			123: hashData,
   297  		},
   298  	}
   299  	release := &Release{
   300  		repository:        NewRepositorySlug("test", "test"),
   301  		ValidationAssetID: 123,
   302  		AssetName:         "foo.tar.xz",
   303  	}
   304  	updater := &Updater{
   305  		source:    source,
   306  		validator: &ChecksumValidator{},
   307  	}
   308  
   309  	data, err := os.ReadFile("testdata/foo.tar.xz")
   310  	require.NoError(t, err)
   311  
   312  	err = updater.validate(context.Background(), release, data)
   313  	require.NoError(t, err)
   314  }
   315  
   316  // ======================== Test UpdateTo with Mock ==========================================
   317  
   318  func TestUpdateToInvalidOwner(t *testing.T) {
   319  	source := &MockSource{}
   320  	updater := &Updater{source: source}
   321  	release := &Release{
   322  		repository: NewRepositorySlug("", "test"),
   323  		AssetID:    123,
   324  	}
   325  	err := updater.UpdateTo(context.Background(), release, "")
   326  	assert.EqualError(t, err, fmt.Sprintf("failed to read asset \"\": %s", ErrIncorrectParameterOwner.Error()))
   327  	assert.ErrorIs(t, err, ErrIncorrectParameterOwner)
   328  }
   329  
   330  func TestUpdateToInvalidRepo(t *testing.T) {
   331  	source := &MockSource{}
   332  	updater := &Updater{source: source}
   333  	release := &Release{
   334  		repository: NewRepositorySlug("test", ""),
   335  		AssetID:    123,
   336  	}
   337  	err := updater.UpdateTo(context.Background(), release, "")
   338  	assert.EqualError(t, err, fmt.Sprintf("failed to read asset \"\": %s", ErrIncorrectParameterRepo.Error()))
   339  	assert.ErrorIs(t, err, ErrIncorrectParameterRepo)
   340  }
   341  
   342  func TestUpdateToReadError(t *testing.T) {
   343  	source := &MockSource{
   344  		readError: true,
   345  		files: map[int64][]byte{
   346  			123: []byte("some data"),
   347  		},
   348  	}
   349  	updater := &Updater{source: source}
   350  	release := &Release{
   351  		repository: NewRepositorySlug("test", "test"),
   352  		AssetID:    123,
   353  	}
   354  	err := updater.UpdateTo(context.Background(), release, "")
   355  	require.Error(t, err)
   356  	assert.True(t, errors.Is(err, errTestRead))
   357  }
   358  
   359  func TestUpdateToWithWrongHash(t *testing.T) {
   360  	data, err := os.ReadFile("testdata/foo.tar.xz")
   361  	require.NoError(t, err)
   362  
   363  	hashData, err := os.ReadFile("testdata/SHA256SUM")
   364  	require.NoError(t, err)
   365  
   366  	source := &MockSource{
   367  		files: map[int64][]byte{
   368  			111: data,
   369  			123: hashData,
   370  		},
   371  	}
   372  	release := &Release{
   373  		repository:        NewRepositorySlug("test", "test"),
   374  		AssetID:           111,
   375  		ValidationAssetID: 123,
   376  		AssetName:         "foo.zip",
   377  	}
   378  	updater := &Updater{
   379  		source:    source,
   380  		validator: &ChecksumValidator{},
   381  	}
   382  
   383  	err = updater.UpdateTo(context.Background(), release, "")
   384  	require.Error(t, err)
   385  	assert.True(t, errors.Is(err, ErrChecksumValidationFailed))
   386  }
   387  
   388  func TestUpdateToSuccess(t *testing.T) {
   389  	data, err := os.ReadFile("testdata/foo.tar.xz")
   390  	require.NoError(t, err)
   391  
   392  	hashData, err := os.ReadFile("testdata/SHA256SUM")
   393  	require.NoError(t, err)
   394  
   395  	source := &MockSource{
   396  		files: map[int64][]byte{
   397  			111: data,
   398  			123: hashData,
   399  		},
   400  	}
   401  	release := &Release{
   402  		repository:        NewRepositorySlug("test", "test"),
   403  		AssetID:           111,
   404  		ValidationAssetID: 123,
   405  		AssetName:         "foo.tar.xz",
   406  	}
   407  	updater := &Updater{
   408  		source:    source,
   409  		validator: &ChecksumValidator{},
   410  	}
   411  
   412  	tempfile := createEmptyFile(t, "foo")
   413  
   414  	err = updater.UpdateTo(context.Background(), release, tempfile)
   415  	require.NoError(t, err)
   416  }
   417  
   418  func TestUpdateToWithMultistepValidationChain(t *testing.T) {
   419  	testVersion := "v0.10.0"
   420  	source, keyRing := mockPGPSourceRepository(t)
   421  	updater, _ := NewUpdater(Config{
   422  		Source:    source,
   423  		Validator: NewChecksumWithPGPValidator("checksums.txt", keyRing),
   424  	})
   425  
   426  	tempFile := createEmptyFile(t, "new_version")
   427  
   428  	getRelease := func(t *testing.T) *Release {
   429  		release, found, err := updater.DetectVersion(context.Background(), testGithubRepository, testVersion)
   430  		require.NotNil(t, release)
   431  		require.NoError(t, err)
   432  		require.True(t, found)
   433  		require.Equal(t, 2, len(release.ValidationChain))
   434  		return release
   435  	}
   436  
   437  	t.Run("Succeeds", func(t *testing.T) {
   438  		release := getRelease(t)
   439  		err := updater.UpdateTo(context.Background(), release, tempFile)
   440  		assert.NoError(t, err)
   441  	})
   442  
   443  	t.Run("ValidationFailInStep1", func(t *testing.T) {
   444  		release := getRelease(t)
   445  		release.ValidationChain[0].ValidationAssetID = 1
   446  
   447  		err := updater.UpdateTo(context.Background(), release, tempFile)
   448  		assert.EqualError(t, err, fmt.Sprintf("failed validating asset content %q: incorrect checksum file format", release.AssetName))
   449  	})
   450  
   451  	t.Run("ValidationFailInStep2", func(t *testing.T) {
   452  		release := getRelease(t)
   453  		release.ValidationChain[1].ValidationAssetID = 1
   454  
   455  		err := updater.UpdateTo(context.Background(), release, tempFile)
   456  		assert.EqualError(t, err, "failed validating asset content \"checksums.txt\": invalid PGP signature")
   457  	})
   458  }
   459  
   460  // createEmptyFile creates an empty file with a unique name in the system temporary folder
   461  func createEmptyFile(t *testing.T, filename string) string {
   462  	t.Helper()
   463  	tempfile := filepath.Join(t.TempDir(), filename)
   464  	t.Logf("use temporary file %q", tempfile)
   465  	file, err := os.OpenFile(tempfile, os.O_WRONLY|os.O_CREATE, 0777)
   466  	if err == nil {
   467  		err = file.Close()
   468  	}
   469  	require.NoError(t, err)
   470  	return tempfile
   471  }
   472  
   473  func setupCurrentVersion(t *testing.T) string {
   474  	t.Helper()
   475  	tmpDir := t.TempDir()
   476  	filename := filepath.Join(tmpDir, "new_version")
   477  	if runtime.GOOS == "windows" {
   478  		filename += ".exe"
   479  	}
   480  
   481  	err := os.WriteFile(filename, []byte("old version"), 0o777)
   482  	require.NoError(t, err)
   483  
   484  	return filename
   485  }
   486  
   487  func assertNewVersion(t *testing.T, filename string) {
   488  	bytes, err := os.ReadFile(filename)
   489  	require.NoError(t, err)
   490  
   491  	assert.Equal(t, []byte("new version!\n"), bytes)
   492  }