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 }