Skip to content

Commit 7ee8df4

Browse files
authored
test(internal/fetch): add tests for error scenarios (#3097)
This change adds a few tests on error conditions during fetch to improve test coverage. It adds a `defaultBackoff` as global var so that test on retries can run faster without affecting prod logic. For #3020
1 parent f6e6117 commit 7ee8df4

File tree

2 files changed

+197
-8
lines changed

2 files changed

+197
-8
lines changed

internal/fetch/fetch.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ import (
3030
"time"
3131
)
3232

33-
var errChecksumMismatch = errors.New("checksum mismatch")
33+
var (
34+
errChecksumMismatch = errors.New("checksum mismatch")
35+
defaultBackoff = 10 * time.Second
36+
)
3437

3538
// Endpoints defines the endpoints used to access GitHub.
3639
type Endpoints struct {
@@ -161,12 +164,11 @@ func DownloadTarball(ctx context.Context, target, url, expectedSha256 string) er
161164
// path. It retries up to 3 times with exponential backoff on failure.
162165
func downloadTarball(ctx context.Context, target, source string) error {
163166
var err error
164-
backoff := 10 * time.Second
165167
for i := range 3 {
166168
if i > 0 {
167169
select {
168-
case <-time.After(backoff):
169-
backoff *= 2
170+
case <-time.After(defaultBackoff):
171+
defaultBackoff *= 2
170172
case <-ctx.Done():
171173
return ctx.Err()
172174
}

internal/fetch/fetch_test.go

Lines changed: 191 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,23 @@ func TestRepoFromTarballLink(t *testing.T) {
5353

5454
func TestRepoFromTarballLinkErrors(t *testing.T) {
5555
for _, test := range []struct {
56+
name string
5657
tarballLink string
5758
}{
58-
{tarballLink: "too-short"},
59+
{
60+
name: "URL path does not match prefix",
61+
tarballLink: "too-short",
62+
},
63+
{
64+
name: "URL path has only one component after prefix removal",
65+
tarballLink: testGitHubDn + "/org",
66+
},
5967
} {
60-
if got, err := RepoFromTarballLink(testGitHubDn, test.tarballLink); err == nil {
61-
t.Errorf("expected an error, got=%v", got)
62-
}
68+
t.Run(test.name, func(t *testing.T) {
69+
if got, err := RepoFromTarballLink(testGitHubDn, test.tarballLink); err == nil {
70+
t.Errorf("expected an error, got=%v", got)
71+
}
72+
})
6373
}
6474
}
6575

@@ -380,3 +390,180 @@ func createTestTarball(t *testing.T, topLevelDir string, files map[string]string
380390
}
381391
return buf.Bytes()
382392
}
393+
394+
func TestExtractTarballErrors(t *testing.T) {
395+
for _, test := range []struct {
396+
name string
397+
tarballPath func(t *testing.T) string // Function to create the test file
398+
dest func(t *testing.T) string
399+
wantErr bool
400+
}{
401+
{
402+
name: "tarball does not exist",
403+
tarballPath: func(t *testing.T) string {
404+
return "non-existent-file.tar.gz"
405+
},
406+
dest: func(t *testing.T) string {
407+
return t.TempDir()
408+
},
409+
wantErr: true,
410+
},
411+
{
412+
name: "not a gzip file",
413+
tarballPath: func(t *testing.T) string {
414+
p := path.Join(t.TempDir(), "file.txt")
415+
if err := os.WriteFile(p, []byte("not a tarball"), 0644); err != nil {
416+
t.Fatal(err)
417+
}
418+
return p
419+
},
420+
dest: func(t *testing.T) string {
421+
return t.TempDir()
422+
},
423+
wantErr: true,
424+
},
425+
{
426+
name: "gzipped but not a tar file",
427+
tarballPath: func(t *testing.T) string {
428+
var buf bytes.Buffer
429+
gw := gzip.NewWriter(&buf)
430+
if _, err := gw.Write([]byte("not a tar file")); err != nil {
431+
t.Fatal(err)
432+
}
433+
if err := gw.Close(); err != nil {
434+
t.Fatal(err)
435+
}
436+
p := path.Join(t.TempDir(), "file.gz")
437+
if err := os.WriteFile(p, buf.Bytes(), 0644); err != nil {
438+
t.Fatal(err)
439+
}
440+
return p
441+
},
442+
dest: func(t *testing.T) string {
443+
return t.TempDir()
444+
},
445+
wantErr: true,
446+
},
447+
{
448+
name: "destination is a file",
449+
tarballPath: func(t *testing.T) string {
450+
tarballData := createTestTarball(t, "repo-abc123", map[string]string{"file.txt": "content"})
451+
p := path.Join(t.TempDir(), "test.tar.gz")
452+
if err := os.WriteFile(p, tarballData, 0644); err != nil {
453+
t.Fatal(err)
454+
}
455+
return p
456+
},
457+
dest: func(t *testing.T) string {
458+
p := path.Join(t.TempDir(), "destfile")
459+
if err := os.WriteFile(p, []byte("i am a file"), 0644); err != nil {
460+
t.Fatal(err)
461+
}
462+
return p
463+
},
464+
wantErr: true,
465+
},
466+
} {
467+
t.Run(test.name, func(t *testing.T) {
468+
err := ExtractTarball(test.tarballPath(t), test.dest(t))
469+
if (err != nil) != test.wantErr {
470+
t.Errorf("ExtractTarball() error = %v, wantErr %v", err, test.wantErr)
471+
}
472+
})
473+
}
474+
}
475+
476+
func TestDownloadTarballErrors(t *testing.T) {
477+
for _, test := range []struct {
478+
name string
479+
target func(t *testing.T) string
480+
url func(t *testing.T) string
481+
sha string
482+
wantErr bool
483+
}{
484+
{
485+
name: "http fails after 3 retries",
486+
target: func(t *testing.T) string {
487+
return path.Join(t.TempDir(), "target")
488+
},
489+
url: func(t *testing.T) string {
490+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
491+
w.WriteHeader(http.StatusInternalServerError)
492+
}))
493+
t.Cleanup(server.Close)
494+
return server.URL
495+
},
496+
sha: "any-sha",
497+
wantErr: true,
498+
}, {
499+
name: "cannot create parent directory",
500+
target: func(t *testing.T) string {
501+
// Create a read-only directory to trigger a permission error.
502+
readOnlyDir := path.Join(t.TempDir(), "read-only")
503+
if err := os.Mkdir(readOnlyDir, 0555); err != nil { // Read and execute only
504+
t.Fatal(err)
505+
}
506+
t.Cleanup(func() {
507+
// Restore permissions so the temp dir can be cleaned up.
508+
os.Chmod(readOnlyDir, 0755)
509+
})
510+
return path.Join(readOnlyDir, "subdir", "target")
511+
},
512+
url: func(t *testing.T) string {
513+
return "https://any-url"
514+
},
515+
sha: "any-sha",
516+
wantErr: true,
517+
},
518+
} {
519+
t.Run(test.name, func(t *testing.T) {
520+
defaultBackoff = time.Millisecond
521+
t.Cleanup(func() {
522+
defaultBackoff = 10 * time.Second
523+
})
524+
err := DownloadTarball(context.Background(), test.target(t), test.url(t), test.sha)
525+
if (err != nil) != test.wantErr {
526+
t.Errorf("DownloadTarball() error = %v, wantErr %v", err, test.wantErr)
527+
}
528+
})
529+
}
530+
}
531+
532+
func TestDownloadTarballRetry(t *testing.T) {
533+
t.Run("succeeds after a few retries", func(t *testing.T) {
534+
// Set a short backoff for this test to speed up retries.
535+
defaultBackoff = time.Millisecond
536+
t.Cleanup(func() {
537+
defaultBackoff = 10 * time.Second
538+
})
539+
testDir := t.TempDir()
540+
tarball := makeTestContents(t)
541+
var requestCount int
542+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
543+
requestCount++
544+
if requestCount < 3 {
545+
w.WriteHeader(http.StatusInternalServerError)
546+
return
547+
}
548+
w.WriteHeader(http.StatusOK)
549+
w.Write(tarball.Contents)
550+
}))
551+
defer server.Close()
552+
553+
target := path.Join(testDir, "target-file")
554+
if err := DownloadTarball(t.Context(), target, server.URL+"/test.tar.gz", tarball.Sha256); err != nil {
555+
t.Fatal(err)
556+
}
557+
558+
if requestCount != 3 {
559+
t.Errorf("expected 3 requests, got %d", requestCount)
560+
}
561+
got, err := os.ReadFile(target)
562+
if err != nil {
563+
t.Fatal(err)
564+
}
565+
if diff := cmp.Diff(tarball.Contents, got); diff != "" {
566+
t.Errorf("mismatch (-want +got):\n%s", diff)
567+
}
568+
})
569+
}

0 commit comments

Comments
 (0)