gloader/grab/client_test.go
2020-12-09 13:29:14 -08:00

916 lines
27 KiB
Go

package grab
import (
"bytes"
"context"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"errors"
"fmt"
"hash"
"io/ioutil"
"math/rand"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/cavaliercoder/grab/grabtest"
)
// TestFilenameResolutions tests that the destination filename for Requests can
// be determined correctly, using an explicitly requested path,
// Content-Disposition headers or a URL path - with or without an existing
// target directory.
func TestFilenameResolution(t *testing.T) {
tests := []struct {
Name string
Filename string
URL string
AttachmentFilename string
Expect string
}{
{"Using Request.Filename", ".testWithFilename", "/url-filename", "header-filename", ".testWithFilename"},
{"Using Content-Disposition Header", "", "/url-filename", ".testWithHeaderFilename", ".testWithHeaderFilename"},
{"Using Content-Disposition Header with target directory", ".test", "/url-filename", "header-filename", ".test/header-filename"},
{"Using URL Path", "", "/.testWithURLFilename?params-filename", "", ".testWithURLFilename"},
{"Using URL Path with target directory", ".test", "/url-filename?garbage", "", ".test/url-filename"},
{"Failure", "", "", "", ""},
}
err := os.Mkdir(".test", 0777)
if err != nil {
panic(err)
}
defer os.RemoveAll(".test")
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
opts := []grabtest.HandlerOption{}
if test.AttachmentFilename != "" {
opts = append(opts, grabtest.AttachmentFilename(test.AttachmentFilename))
}
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(test.Filename, url+test.URL)
resp := DefaultClient.Do(req)
defer os.Remove(resp.Filename)
if err := resp.Err(); err != nil {
if test.Expect != "" || err != ErrNoFilename {
panic(err)
}
} else {
if test.Expect == "" {
t.Errorf("expected: %v, got: %v", ErrNoFilename, err)
}
}
if resp.Filename != test.Expect {
t.Errorf("Filename mismatch. Expected '%s', got '%s'.", test.Expect, resp.Filename)
}
testComplete(t, resp)
}, opts...)
})
}
}
// TestChecksums checks that checksum validation behaves as expected for valid
// and corrupted downloads.
func TestChecksums(t *testing.T) {
tests := []struct {
size int
hash hash.Hash
sum string
match bool
}{
{128, md5.New(), "37eff01866ba3f538421b30b7cbefcac", true},
{128, md5.New(), "37eff01866ba3f538421b30b7cbefcad", false},
{1024, md5.New(), "b2ea9f7fcea831a4a63b213f41a8855b", true},
{1024, md5.New(), "b2ea9f7fcea831a4a63b213f41a8855c", false},
{1048576, md5.New(), "c35cc7d8d91728a0cb052831bc4ef372", true},
{1048576, md5.New(), "c35cc7d8d91728a0cb052831bc4ef373", false},
{128, sha1.New(), "e6434bc401f98603d7eda504790c98c67385d535", true},
{128, sha1.New(), "e6434bc401f98603d7eda504790c98c67385d536", false},
{1024, sha1.New(), "5b00669c480d5cffbdfa8bdba99561160f2d1b77", true},
{1024, sha1.New(), "5b00669c480d5cffbdfa8bdba99561160f2d1b78", false},
{1048576, sha1.New(), "ecfc8e86fdd83811f9cc9bf500993b63069923be", true},
{1048576, sha1.New(), "ecfc8e86fdd83811f9cc9bf500993b63069923bf", false},
{128, sha256.New(), "471fb943aa23c511f6f72f8d1652d9c880cfa392ad80503120547703e56a2be5", true},
{128, sha256.New(), "471fb943aa23c511f6f72f8d1652d9c880cfa392ad80503120547703e56a2be4", false},
{1024, sha256.New(), "785b0751fc2c53dc14a4ce3d800e69ef9ce1009eb327ccf458afe09c242c26c9", true},
{1024, sha256.New(), "785b0751fc2c53dc14a4ce3d800e69ef9ce1009eb327ccf458afe09c242c26c8", false},
{1048576, sha256.New(), "fbbab289f7f94b25736c58be46a994c441fd02552cc6022352e3d86d2fab7c83", true},
{1048576, sha256.New(), "fbbab289f7f94b25736c58be46a994c441fd02552cc6022352e3d86d2fab7c82", false},
{128, sha512.New(), "1dffd5e3adb71d45d2245939665521ae001a317a03720a45732ba1900ca3b8351fc5c9b4ca513eba6f80bc7b1d1fdad4abd13491cb824d61b08d8c0e1561b3f7", true},
{128, sha512.New(), "1dffd5e3adb71d45d2245939665521ae001a317a03720a45732ba1900ca3b8351fc5c9b4ca513eba6f80bc7b1d1fdad4abd13491cb824d61b08d8c0e1561b3f8", false},
{1024, sha512.New(), "37f652be867f28ed033269cbba201af2112c2b3fd334a89fd2f757938ddee815787cc61d6e24a8a33340d0f7e86ffc058816b88530766ba6e231620a130b566c", true},
{1024, sha512.New(), "37f652bf867f28ed033269cbba201af2112c2b3fd334a89fd2f757938ddee815787cc61d6e24a8a33340d0f7e86ffc058816b88530766ba6e231620a130b566d", false},
{1048576, sha512.New(), "ac1d097b4ea6f6ad7ba640275b9ac290e4828cd760a0ebf76d555463a4f505f95df4f611629539a2dd1848e7c1304633baa1826462b3c87521c0c6e3469b67af", true},
{1048576, sha512.New(), "ac1d097c4ea6f6ad7ba640275b9ac290e4828cd760a0ebf76d555463a4f505f95df4f611629539a2dd1848e7c1304633baa1826462b3c87521c0c6e3469b67af", false},
}
for _, test := range tests {
var expect error
comparison := "Match"
if !test.match {
comparison = "Mismatch"
expect = ErrBadChecksum
}
t.Run(fmt.Sprintf("With%s%s", comparison, test.sum[:8]), func(t *testing.T) {
filename := fmt.Sprintf(".testChecksum-%s-%s", comparison, test.sum[:8])
defer os.Remove(filename)
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(filename, url)
req.SetChecksum(test.hash, grabtest.MustHexDecodeString(test.sum), true)
resp := DefaultClient.Do(req)
err := resp.Err()
if err != expect {
t.Errorf("expected error: %v, got: %v", expect, err)
}
// ensure mismatch file was deleted
if !test.match {
if _, err := os.Stat(filename); err == nil {
t.Errorf("checksum failure not cleaned up: %s", filename)
} else if !os.IsNotExist(err) {
panic(err)
}
}
testComplete(t, resp)
}, grabtest.ContentLength(test.size))
})
}
}
// TestContentLength ensures that ErrBadLength is returned if a server response
// does not match the requested length.
func TestContentLength(t *testing.T) {
size := int64(32768)
testCases := []struct {
Name string
NoHead bool
Size int64
Expect int64
Match bool
}{
{"Good size in HEAD request", false, size, size, true},
{"Good size in GET request", true, size, size, true},
{"Bad size in HEAD request", false, size - 1, size, false},
{"Bad size in GET request", true, size - 1, size, false},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
opts := []grabtest.HandlerOption{
grabtest.ContentLength(int(test.Size)),
}
if test.NoHead {
opts = append(opts, grabtest.MethodWhitelist("GET"))
}
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(".testSize-mismatch-head", url)
req.Size = size
resp := DefaultClient.Do(req)
defer os.Remove(resp.Filename)
err := resp.Err()
if test.Match {
if err == ErrBadLength {
t.Errorf("error: %v", err)
} else if err != nil {
panic(err)
} else if resp.Size() != size {
t.Errorf("expected %v bytes, got %v bytes", size, resp.Size())
}
} else {
if err == nil {
t.Errorf("expected: %v, got %v", ErrBadLength, err)
} else if err != ErrBadLength {
panic(err)
}
}
testComplete(t, resp)
}, opts...)
})
}
}
// TestAutoResume tests segmented downloading of a large file.
func TestAutoResume(t *testing.T) {
segs := 8
size := 1048576
sum := grabtest.DefaultHandlerSHA256ChecksumBytes //grabtest.MustHexDecodeString("fbbab289f7f94b25736c58be46a994c441fd02552cc6022352e3d86d2fab7c83")
filename := ".testAutoResume"
defer os.Remove(filename)
for i := 0; i < segs; i++ {
segsize := (i + 1) * (size / segs)
t.Run(fmt.Sprintf("With%vBytes", segsize), func(t *testing.T) {
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(filename, url)
if i == segs-1 {
req.SetChecksum(sha256.New(), sum, false)
}
resp := mustDo(req)
if i > 0 && !resp.DidResume {
t.Errorf("expected Response.DidResume to be true")
}
testComplete(t, resp)
},
grabtest.ContentLength(segsize),
)
})
}
t.Run("WithFailure", func(t *testing.T) {
grabtest.WithTestServer(t, func(url string) {
// request smaller segment
req := mustNewRequest(filename, url)
resp := DefaultClient.Do(req)
if err := resp.Err(); err != ErrBadLength {
t.Errorf("expected ErrBadLength for smaller request, got: %v", err)
}
},
grabtest.ContentLength(size-128),
)
})
t.Run("WithNoResume", func(t *testing.T) {
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(filename, url)
req.NoResume = true
resp := mustDo(req)
if resp.DidResume {
t.Errorf("expected Response.DidResume to be false")
}
testComplete(t, resp)
},
grabtest.ContentLength(size+128),
)
})
t.Run("WithNoResumeAndTruncate", func(t *testing.T) {
size := size - 128
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(filename, url)
req.NoResume = true
resp := mustDo(req)
if resp.DidResume {
t.Errorf("expected Response.DidResume to be false")
}
if v := resp.BytesComplete(); v != int64(size) {
t.Errorf("expected Response.BytesComplete: %d, got: %d", size, v)
}
testComplete(t, resp)
},
grabtest.ContentLength(size),
)
})
t.Run("WithNoContentLengthHeader", func(t *testing.T) {
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(filename, url)
req.SetChecksum(sha256.New(), sum, false)
resp := mustDo(req)
if !resp.DidResume {
t.Errorf("expected Response.DidResume to be true")
}
if actual := resp.Size(); actual != int64(size) {
t.Errorf("expected Response.Size: %d, got: %d", size, actual)
}
testComplete(t, resp)
},
grabtest.ContentLength(size),
grabtest.HeaderBlacklist("Content-Length"),
)
})
t.Run("WithNoContentLengthHeaderAndChecksumFailure", func(t *testing.T) {
// ref: https://github.com/cavaliercoder/grab/pull/27
size := size * 2
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(filename, url)
req.SetChecksum(sha256.New(), sum, false)
resp := DefaultClient.Do(req)
if err := resp.Err(); err != ErrBadChecksum {
t.Errorf("expected error: %v, got: %v", ErrBadChecksum, err)
}
if !resp.DidResume {
t.Errorf("expected Response.DidResume to be true")
}
if actual := resp.BytesComplete(); actual != int64(size) {
t.Errorf("expected Response.BytesComplete: %d, got: %d", size, actual)
}
if actual := resp.Size(); actual != int64(size) {
t.Errorf("expected Response.Size: %d, got: %d", size, actual)
}
testComplete(t, resp)
},
grabtest.ContentLength(size),
grabtest.HeaderBlacklist("Content-Length"),
)
})
// TODO: test when existing file is corrupted
}
func TestSkipExisting(t *testing.T) {
filename := ".testSkipExisting"
defer os.Remove(filename)
// download a file
grabtest.WithTestServer(t, func(url string) {
resp := mustDo(mustNewRequest(filename, url))
testComplete(t, resp)
})
// redownload
grabtest.WithTestServer(t, func(url string) {
resp := mustDo(mustNewRequest(filename, url))
testComplete(t, resp)
// ensure download was resumed
if !resp.DidResume {
t.Fatalf("Expected download to skip existing file, but it did not")
}
// ensure all bytes were resumed
if resp.Size() == 0 || resp.Size() != resp.bytesResumed {
t.Fatalf("Expected to skip %d bytes in redownload; got %d", resp.Size(), resp.bytesResumed)
}
})
// ensure checksum is performed on pre-existing file
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(filename, url)
req.SetChecksum(sha256.New(), []byte{0x01, 0x02, 0x03, 0x04}, true)
resp := DefaultClient.Do(req)
if err := resp.Err(); err != ErrBadChecksum {
t.Fatalf("Expected checksum error, got: %v", err)
}
})
}
// TestBatch executes multiple requests simultaneously and validates the
// responses.
func TestBatch(t *testing.T) {
tests := 32
size := 32768
sum := grabtest.MustHexDecodeString("e11360251d1173650cdcd20f111d8f1ca2e412f572e8b36a4dc067121c1799b8")
// test with 4 workers and with one per request
grabtest.WithTestServer(t, func(url string) {
for _, workerCount := range []int{4, 0} {
// create requests
reqs := make([]*Request, tests)
for i := 0; i < len(reqs); i++ {
filename := fmt.Sprintf(".testBatch.%d", i+1)
reqs[i] = mustNewRequest(filename, url+fmt.Sprintf("/request_%d?", i+1))
reqs[i].Label = fmt.Sprintf("Test %d", i+1)
reqs[i].SetChecksum(sha256.New(), sum, false)
}
// batch run
responses := DefaultClient.DoBatch(workerCount, reqs...)
// listen for responses
Loop:
for i := 0; i < len(reqs); {
select {
case resp := <-responses:
if resp == nil {
break Loop
}
testComplete(t, resp)
if err := resp.Err(); err != nil {
t.Errorf("%s: %v", resp.Filename, err)
}
// remove test file
if resp.IsComplete() {
os.Remove(resp.Filename) // ignore errors
}
i++
}
}
}
},
grabtest.ContentLength(size),
)
}
// TestCancelContext tests that a batch of requests can be cancel using a
// context.Context cancellation. Requests are cancelled in multiple states:
// in-progress and unstarted.
func TestCancelContext(t *testing.T) {
fileSize := 134217728
tests := 256
client := NewClient()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
grabtest.WithTestServer(t, func(url string) {
reqs := make([]*Request, tests)
for i := 0; i < tests; i++ {
req := mustNewRequest("", fmt.Sprintf("%s/.testCancelContext%d", url, i))
reqs[i] = req.WithContext(ctx)
}
respch := client.DoBatch(8, reqs...)
time.Sleep(time.Millisecond * 500)
cancel()
for resp := range respch {
defer os.Remove(resp.Filename)
// err should be context.Canceled or http.errRequestCanceled
if resp.Err() == nil || !strings.Contains(resp.Err().Error(), "canceled") {
t.Errorf("expected '%v', got '%v'", context.Canceled, resp.Err())
}
if resp.BytesComplete() >= int64(fileSize) {
t.Errorf("expected Response.BytesComplete: < %d, got: %d", fileSize, resp.BytesComplete())
}
}
},
grabtest.ContentLength(fileSize),
)
}
// TestCancelHangingResponse tests that a never ending request is terminated
// when the response is cancelled.
func TestCancelHangingResponse(t *testing.T) {
fileSize := 10
client := NewClient()
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest("", fmt.Sprintf("%s/.testCancelHangingResponse", url))
resp := client.Do(req)
defer os.Remove(resp.Filename)
// Wait for some bytes to be transferred
for resp.BytesComplete() == 0 {
time.Sleep(50 * time.Millisecond)
}
done := make(chan error)
go func() {
done <- resp.Cancel()
}()
select {
case err := <-done:
if err != context.Canceled {
t.Errorf("Expected context.Canceled error, go: %v", err)
}
case <-time.After(time.Second):
t.Fatal("response was not cancelled within 1s")
}
if resp.BytesComplete() == int64(fileSize) {
t.Error("download was not supposed to be complete")
}
fmt.Println("bye")
},
grabtest.RateLimiter(1),
grabtest.ContentLength(fileSize),
)
}
// TestNestedDirectory tests that missing subdirectories are created.
func TestNestedDirectory(t *testing.T) {
dir := "./.testNested/one/two/three"
filename := ".testNestedFile"
expect := dir + "/" + filename
t.Run("Create", func(t *testing.T) {
grabtest.WithTestServer(t, func(url string) {
resp := mustDo(mustNewRequest(expect, url+"/"+filename))
defer os.RemoveAll("./.testNested/")
if resp.Filename != expect {
t.Errorf("expected nested Request.Filename to be %v, got %v", expect, resp.Filename)
}
})
})
t.Run("No create", func(t *testing.T) {
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(expect, url+"/"+filename)
req.NoCreateDirectories = true
resp := DefaultClient.Do(req)
err := resp.Err()
if !os.IsNotExist(err) {
t.Errorf("expected: %v, got: %v", os.ErrNotExist, err)
}
})
})
}
// TestRemoteTime tests that the timestamp of the downloaded file can be set
// according to the timestamp of the remote file.
func TestRemoteTime(t *testing.T) {
filename := "./.testRemoteTime"
defer os.Remove(filename)
// random time between epoch and now
expect := time.Unix(rand.Int63n(time.Now().Unix()), 0)
grabtest.WithTestServer(t, func(url string) {
resp := mustDo(mustNewRequest(filename, url))
fi, err := os.Stat(resp.Filename)
if err != nil {
panic(err)
}
actual := fi.ModTime()
if !actual.Equal(expect) {
t.Errorf("expected %v, got %v", expect, actual)
}
},
grabtest.LastModified(expect),
)
}
func TestResponseCode(t *testing.T) {
filename := "./.testResponseCode"
t.Run("With404", func(t *testing.T) {
defer os.Remove(filename)
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(filename, url)
resp := DefaultClient.Do(req)
expect := StatusCodeError(http.StatusNotFound)
err := resp.Err()
if err != expect {
t.Errorf("expected %v, got '%v'", expect, err)
}
if !IsStatusCodeError(err) {
t.Errorf("expected IsStatusCodeError to return true for %T: %v", err, err)
}
},
grabtest.StatusCodeStatic(http.StatusNotFound),
)
})
t.Run("WithIgnoreNon2XX", func(t *testing.T) {
defer os.Remove(filename)
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(filename, url)
req.IgnoreBadStatusCodes = true
resp := DefaultClient.Do(req)
if err := resp.Err(); err != nil {
t.Errorf("expected nil, got '%v'", err)
}
},
grabtest.StatusCodeStatic(http.StatusNotFound),
)
})
}
func TestBeforeCopyHook(t *testing.T) {
filename := "./.testBeforeCopy"
t.Run("Noop", func(t *testing.T) {
defer os.RemoveAll(filename)
grabtest.WithTestServer(t, func(url string) {
called := false
req := mustNewRequest(filename, url)
req.BeforeCopy = func(resp *Response) error {
called = true
if resp.IsComplete() {
t.Error("Response object passed to BeforeCopy hook has already been closed")
}
if resp.Progress() != 0 {
t.Error("Download progress already > 0 when BeforeCopy hook was called")
}
if resp.Duration() == 0 {
t.Error("Duration was zero when BeforeCopy was called")
}
if resp.BytesComplete() != 0 {
t.Error("BytesComplete already > 0 when BeforeCopy hook was called")
}
return nil
}
resp := DefaultClient.Do(req)
if err := resp.Err(); err != nil {
t.Errorf("unexpected error using BeforeCopy hook: %v", err)
}
testComplete(t, resp)
if !called {
t.Error("BeforeCopy hook was never called")
}
})
})
t.Run("WithError", func(t *testing.T) {
defer os.RemoveAll(filename)
grabtest.WithTestServer(t, func(url string) {
testError := errors.New("test")
req := mustNewRequest(filename, url)
req.BeforeCopy = func(resp *Response) error {
return testError
}
resp := DefaultClient.Do(req)
if err := resp.Err(); err != testError {
t.Errorf("expected error '%v', got '%v'", testError, err)
}
if resp.BytesComplete() != 0 {
t.Errorf("expected 0 bytes completed for canceled BeforeCopy hook, got %d",
resp.BytesComplete())
}
testComplete(t, resp)
})
})
// Assert that an existing local file will not be truncated prior to the
// BeforeCopy hook has a chance to cancel the request
t.Run("NoTruncate", func(t *testing.T) {
tfile, err := ioutil.TempFile("", "grab_client_test.*.file")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tfile.Name())
const size = 128
_, err = tfile.Write(bytes.Repeat([]byte("x"), size))
if err != nil {
t.Fatal(err)
}
grabtest.WithTestServer(t, func(url string) {
called := false
req := mustNewRequest(tfile.Name(), url)
req.NoResume = true
req.BeforeCopy = func(resp *Response) error {
called = true
fi, err := tfile.Stat()
if err != nil {
t.Errorf("failed to stat temp file: %v", err)
return nil
}
if fi.Size() != size {
t.Errorf("expected existing file size of %d bytes "+
"prior to BeforeCopy hook, got %d", size, fi.Size())
}
return nil
}
resp := DefaultClient.Do(req)
if err := resp.Err(); err != nil {
t.Errorf("unexpected error using BeforeCopy hook: %v", err)
}
testComplete(t, resp)
if !called {
t.Error("BeforeCopy hook was never called")
}
})
})
}
func TestAfterCopyHook(t *testing.T) {
filename := "./.testAfterCopy"
t.Run("Noop", func(t *testing.T) {
defer os.RemoveAll(filename)
grabtest.WithTestServer(t, func(url string) {
called := false
req := mustNewRequest(filename, url)
req.AfterCopy = func(resp *Response) error {
called = true
if resp.IsComplete() {
t.Error("Response object passed to AfterCopy hook has already been closed")
}
if resp.Progress() <= 0 {
t.Error("Download progress was 0 when AfterCopy hook was called")
}
if resp.Duration() == 0 {
t.Error("Duration was zero when AfterCopy was called")
}
if resp.BytesComplete() <= 0 {
t.Error("BytesComplete was 0 when AfterCopy hook was called")
}
return nil
}
resp := DefaultClient.Do(req)
if err := resp.Err(); err != nil {
t.Errorf("unexpected error using AfterCopy hook: %v", err)
}
testComplete(t, resp)
if !called {
t.Error("AfterCopy hook was never called")
}
})
})
t.Run("WithError", func(t *testing.T) {
defer os.RemoveAll(filename)
grabtest.WithTestServer(t, func(url string) {
testError := errors.New("test")
req := mustNewRequest(filename, url)
req.AfterCopy = func(resp *Response) error {
return testError
}
resp := DefaultClient.Do(req)
if err := resp.Err(); err != testError {
t.Errorf("expected error '%v', got '%v'", testError, err)
}
if resp.BytesComplete() <= 0 {
t.Errorf("ByteCompleted was %d after AfterCopy hook was called",
resp.BytesComplete())
}
testComplete(t, resp)
})
})
}
func TestIssue37(t *testing.T) {
// ref: https://github.com/cavaliercoder/grab/issues/37
filename := "./.testIssue37"
largeSize := int64(2097152)
smallSize := int64(1048576)
defer os.RemoveAll(filename)
// download large file
grabtest.WithTestServer(t, func(url string) {
resp := mustDo(mustNewRequest(filename, url))
if resp.Size() != largeSize {
t.Errorf("expected response size: %d, got: %d", largeSize, resp.Size())
}
}, grabtest.ContentLength(int(largeSize)))
// download new, smaller version of same file
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(filename, url)
req.NoResume = true
resp := mustDo(req)
if resp.Size() != smallSize {
t.Errorf("expected response size: %d, got: %d", smallSize, resp.Size())
}
// local file should have truncated and not resumed
if resp.DidResume {
t.Errorf("expected download to truncate, resumed instead")
}
}, grabtest.ContentLength(int(smallSize)))
fi, err := os.Stat(filename)
if err != nil {
t.Fatal(err)
}
if fi.Size() != int64(smallSize) {
t.Errorf("expected file size %d, got %d", smallSize, fi.Size())
}
}
// TestHeadBadStatus validates that HEAD requests that return non-200 can be
// ignored and succeed if the GET requests succeeeds.
//
// Fixes: https://github.com/cavaliercoder/grab/issues/43
func TestHeadBadStatus(t *testing.T) {
expect := http.StatusOK
filename := ".testIssue43"
statusFunc := func(r *http.Request) int {
if r.Method == "HEAD" {
return http.StatusForbidden
}
return http.StatusOK
}
grabtest.WithTestServer(t, func(url string) {
testURL := fmt.Sprintf("%s/%s", url, filename)
resp := mustDo(mustNewRequest("", testURL))
if resp.HTTPResponse.StatusCode != expect {
t.Errorf(
"expected status code: %d, got:% d",
expect,
resp.HTTPResponse.StatusCode)
}
},
grabtest.StatusCode(statusFunc),
)
}
// TestMissingContentLength ensures that the Response.Size is correct for
// transfers where the remote server does not send a Content-Length header.
//
// TestAutoResume also covers cases with checksum validation.
//
// Kudos to Setnička Jiří <Jiri.Setnicka@ysoft.com> for identifying and raising
// a solution to this issue. Ref: https://github.com/cavaliercoder/grab/pull/27
func TestMissingContentLength(t *testing.T) {
// expectSize must be sufficiently large that DefaultClient.Do won't prefetch
// the entire body and compute ContentLength before returning a Response.
expectSize := 1048576
opts := []grabtest.HandlerOption{
grabtest.ContentLength(expectSize),
grabtest.HeaderBlacklist("Content-Length"),
grabtest.TimeToFirstByte(time.Millisecond * 100), // delay for initial read
}
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(".testMissingContentLength", url)
req.SetChecksum(
md5.New(),
grabtest.DefaultHandlerMD5ChecksumBytes,
false)
resp := DefaultClient.Do(req)
// ensure remote server is not sending content-length header
if v := resp.HTTPResponse.Header.Get("Content-Length"); v != "" {
panic(fmt.Sprintf("http header content length must be empty, got: %s", v))
}
if v := resp.HTTPResponse.ContentLength; v != -1 {
panic(fmt.Sprintf("http response content length must be -1, got: %d", v))
}
// before completion, response size should be -1
if resp.Size() != -1 {
t.Errorf("expected response size: -1, got: %d", resp.Size())
}
// block for completion
if err := resp.Err(); err != nil {
panic(err)
}
// on completion, response size should be actual transfer size
if resp.Size() != int64(expectSize) {
t.Errorf("expected response size: %d, got: %d", expectSize, resp.Size())
}
}, opts...)
}
func TestNoStore(t *testing.T) {
filename := ".testSubdir/testNoStore"
t.Run("DefaultCase", func(t *testing.T) {
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest(filename, url)
req.NoStore = true
req.SetChecksum(md5.New(), grabtest.DefaultHandlerMD5ChecksumBytes, true)
resp := mustDo(req)
// ensure Response.Bytes is correct and can be reread
b, err := resp.Bytes()
if err != nil {
panic(err)
}
grabtest.AssertSHA256Sum(
t,
grabtest.DefaultHandlerSHA256ChecksumBytes,
bytes.NewReader(b),
)
// ensure Response.Open stream is correct and can be reread
r, err := resp.Open()
if err != nil {
panic(err)
}
defer r.Close()
grabtest.AssertSHA256Sum(
t,
grabtest.DefaultHandlerSHA256ChecksumBytes,
r,
)
// Response.Filename should still be set
if resp.Filename != filename {
t.Errorf("expected Response.Filename: %s, got: %s", filename, resp.Filename)
}
// ensure no files were written
paths := []string{
filename,
filepath.Base(filename),
filepath.Dir(filename),
resp.Filename,
filepath.Base(resp.Filename),
filepath.Dir(resp.Filename),
}
for _, path := range paths {
_, err := os.Stat(path)
if !os.IsNotExist(err) {
t.Errorf(
"expect error: %v, got: %v, for path: %s",
os.ErrNotExist,
err,
path)
}
}
})
})
t.Run("ChecksumValidation", func(t *testing.T) {
grabtest.WithTestServer(t, func(url string) {
req := mustNewRequest("", url)
req.NoStore = true
req.SetChecksum(
md5.New(),
grabtest.MustHexDecodeString("deadbeefcafebabe"),
true)
resp := DefaultClient.Do(req)
if err := resp.Err(); err != ErrBadChecksum {
t.Errorf("expected error: %v, got: %v", ErrBadChecksum, err)
}
})
})
}