From 1f2f99d4d629add3951b81f60f0cb1529d2776a0 Mon Sep 17 00:00:00 2001 From: Janne Snabb Date: Sun, 19 Apr 2026 00:53:53 +0300 Subject: [PATCH] Add tests for coverage gaps --- httpreaderat_test.go | 170 ++++++++++++++++++++++++++++++++++++++++++ meta_test.go | 53 ++++++++++++++ store_test.go | 171 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 394 insertions(+) create mode 100644 meta_test.go create mode 100644 store_test.go diff --git a/httpreaderat_test.go b/httpreaderat_test.go index 3a5a73f..4cf4687 100644 --- a/httpreaderat_test.go +++ b/httpreaderat_test.go @@ -59,6 +59,8 @@ func (ra *readerAtFixture) TestRangeSupportInitial() { ra.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rnge := r.Header.Get("Range") ra.Equal(rnge, "bytes=0-0") + w.Header().Set("Content-Type", "application/zip") + w.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT") w.Header().Set("Content-Range", fmt.Sprintf(" bytes 0-0/%d", 1)) w.WriteHeader(http.StatusPartialContent) w.Write([]byte{17}) @@ -67,6 +69,9 @@ func (ra *readerAtFixture) TestRangeSupportInitial() { reader, err := ra.reader() ra.Nil(err) ra.NotNil(reader) + ra.Equal("application/zip", reader.ContentType()) + ra.Equal("Wed, 21 Oct 2015 07:28:00 GMT", reader.LastModified()) + ra.Equal(int64(1), reader.Size()) } func (ra *readerAtFixture) TestRangeSupportInitialEmptyResponse() { @@ -150,3 +155,168 @@ func (ra *readerAtFixture) TestParallelFiles() { wg.Wait() } + +func (ra *readerAtFixture) TestReadAtEOFClamp() { + content := []byte("hello") + ra.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Header.Get("Range") { + case "bytes=0-0": + w.Header().Set("Content-Range", "bytes 0-0/5") + w.WriteHeader(http.StatusPartialContent) + w.Write(content[:1]) + case "bytes=3-4": + w.Header().Set("Content-Range", "bytes 3-4/5") + w.WriteHeader(http.StatusPartialContent) + w.Write(content[3:]) + default: + ra.FailNow("unexpected range", r.Header.Get("Range")) + } + })) + + reader, err := ra.reader() + ra.NoError(err) + + buf := make([]byte, 4) + n, err := reader.ReadAt(buf, 3) + ra.Equal(2, n) + ra.ErrorIs(err, io.EOF) + ra.Equal("lo", string(buf[:n])) +} + +func (ra *readerAtFixture) TestReadAtPastEOF() { + content := []byte("hello") + ra.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ra.Equal("bytes=0-0", r.Header.Get("Range")) + w.Header().Set("Content-Range", "bytes 0-0/5") + w.WriteHeader(http.StatusPartialContent) + w.Write(content[:1]) + })) + + reader, err := ra.reader() + ra.NoError(err) + + n, err := reader.ReadAt(make([]byte, 1), int64(len(content))) + ra.Equal(0, n) + ra.ErrorIs(err, io.EOF) +} + +func (ra *readerAtFixture) TestValidationFailure() { + etag := `"v1"` + ra.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Header.Get("Range") { + case "bytes=0-0": + w.Header().Set("Content-Range", "bytes 0-0/5") + w.Header().Set("ETag", etag) + w.WriteHeader(http.StatusPartialContent) + w.Write([]byte("h")) + case "bytes=1-1": + w.Header().Set("Content-Range", "bytes 1-1/5") + w.Header().Set("ETag", `"v2"`) + w.WriteHeader(http.StatusPartialContent) + w.Write([]byte("e")) + default: + ra.FailNow("unexpected range", r.Header.Get("Range")) + } + })) + + reader, err := ra.reader() + ra.NoError(err) + + n, err := reader.ReadAt(make([]byte, 1), 1) + ra.Equal(0, n) + ra.ErrorIs(err, httpreaderat.ErrValidationFailed) +} + +func (ra *readerAtFixture) TestMissingContentRange() { + ra.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusPartialContent) + w.Write([]byte("h")) + })) + + reader, err := ra.reader() + ra.EqualError(err, "no content-range header in partial response") + ra.Nil(reader) +} + +func (ra *readerAtFixture) TestMalformedContentRange() { + ra.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Range", "banana") + w.WriteHeader(http.StatusPartialContent) + w.Write([]byte("h")) + })) + + reader, err := ra.reader() + ra.EqualError(err, "http request: unsupported unit") + ra.Nil(reader) +} + +func (ra *readerAtFixture) TestDifferentRangeThanRequested() { + ra.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Range", "bytes 1-1/5") + w.WriteHeader(http.StatusPartialContent) + w.Write([]byte("h")) + })) + + reader, err := ra.reader() + ra.EqualError(err, "received different range than requested (req=0-0, resp=1-1)") + ra.Nil(reader) +} + +func (ra *readerAtFixture) TestHTTPRequestFailure() { + req, err := http.NewRequest("GET", "http://127.0.0.1:1/file.zip", nil) + ra.NoError(err) + + reader, err := httpreaderat.New(&http.Client{}, req, nil) + ra.Nil(reader) + ra.Error(err) +} + +func (ra *readerAtFixture) TestServerStopsSupportingRangeRequests() { + requests := 0 + ra.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + if requests == 1 { + w.Header().Set("Content-Range", "bytes 0-0/5") + w.WriteHeader(http.StatusPartialContent) + w.Write([]byte("h")) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte("hello")) + })) + + req, err := http.NewRequest("GET", ra.server.URL+"/file.zip", nil) + ra.NoError(err) + + reader, err := httpreaderat.New(nil, req, httpreaderat.NewStoreMemory()) + ra.NoError(err) + + n, err := reader.ReadAt(make([]byte, 1), 1) + ra.Equal(0, n) + ra.EqualError(err, "server suddenly stopped supporting range requests") +} + +func (ra *readerAtFixture) TestFallbackStoreWhenRangeNotSupported() { + content := []byte("hello") + ra.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT") + w.WriteHeader(http.StatusOK) + w.Write(content) + })) + + req, err := http.NewRequest("GET", ra.server.URL+"/file.zip", nil) + ra.NoError(err) + + reader, err := httpreaderat.New(nil, req, httpreaderat.NewStoreMemory()) + ra.NoError(err) + ra.Equal("text/plain", reader.ContentType()) + ra.Equal("Wed, 21 Oct 2015 07:28:00 GMT", reader.LastModified()) + ra.Equal(int64(len(content)), reader.Size()) + + buf := make([]byte, len(content)) + n, err := reader.ReadAt(buf, 0) + ra.NoError(err) + ra.Equal(len(content), n) + ra.Equal(content, buf) +} diff --git a/meta_test.go b/meta_test.go new file mode 100644 index 0000000..fac0f22 --- /dev/null +++ b/meta_test.go @@ -0,0 +1,53 @@ +package httpreaderat + +import ( + "net/http" + "testing" +) + +func TestValidateIgnoresContentTypeChanges(t *testing.T) { + ra := &HTTPReaderAt{ + meta: meta{ + size: 5, + contentType: "text/plain", + }, + } + + resp := &http.Response{ + StatusCode: http.StatusPartialContent, + Header: http.Header{ + "Content-Range": []string{"bytes 0-0/5"}, + "Content-Type": []string{"application/octet-stream"}, + }, + } + + if err := ra.validate(resp); err != nil { + t.Fatalf("validate() error = %v, want nil", err) + } +} + +func TestGetMetaFromStatusOK(t *testing.T) { + resp := &http.Response{ + StatusCode: http.StatusOK, + ContentLength: 42, + Header: http.Header{ + "Content-Type": []string{"application/zip"}, + "Last-Modified": []string{"Wed, 21 Oct 2015 07:28:00 GMT"}, + "Etag": []string{`"v1"`}, + }, + } + + got := getMeta(resp) + if got.size != 42 { + t.Fatalf("size = %d, want 42", got.size) + } + if got.contentType != "application/zip" { + t.Fatalf("contentType = %q, want %q", got.contentType, "application/zip") + } + if got.lastModified != "Wed, 21 Oct 2015 07:28:00 GMT" { + t.Fatalf("lastModified = %q", got.lastModified) + } + if got.etag != `"v1"` { + t.Fatalf("etag = %q, want %q", got.etag, `"v1"`) + } +} diff --git a/store_test.go b/store_test.go new file mode 100644 index 0000000..e64f61e --- /dev/null +++ b/store_test.go @@ -0,0 +1,171 @@ +package httpreaderat + +import ( + "bytes" + "errors" + "io" + "os" + "testing" +) + +func TestStoreMemory(t *testing.T) { + s := NewStoreMemory() + + if n, err := s.ReadAt(make([]byte, 1), 0); n != 0 || err != nil { + t.Fatalf("empty ReadAt = (%d, %v), want (0, nil)", n, err) + } + + n, err := s.ReadFrom(bytes.NewBufferString("hello world")) + if err != nil { + t.Fatalf("ReadFrom() error = %v", err) + } + if n != 11 { + t.Fatalf("ReadFrom() n = %d, want 11", n) + } + if got := s.Size(); got != 11 { + t.Fatalf("Size() = %d, want 11", got) + } + + buf := make([]byte, 5) + nn, err := s.ReadAt(buf, 6) + if err != nil { + t.Fatalf("ReadAt() error = %v", err) + } + if nn != 5 || string(buf) != "world" { + t.Fatalf("ReadAt() = (%d, %q), want (5, %q)", nn, string(buf), "world") + } + + if err := s.Close(); err != nil { + t.Fatalf("Close() error = %v", err) + } + if got := s.Size(); got != 0 { + t.Fatalf("Size() after Close = %d, want 0", got) + } +} + +func TestStoreFile(t *testing.T) { + s := NewStoreFile() + + if n, err := s.ReadAt(make([]byte, 1), 0); n != 0 || err != nil { + t.Fatalf("empty ReadAt = (%d, %v), want (0, nil)", n, err) + } + + n, err := s.ReadFrom(bytes.NewBufferString("abcdef")) + if err != nil { + t.Fatalf("ReadFrom() error = %v", err) + } + if n != 6 { + t.Fatalf("ReadFrom() n = %d, want 6", n) + } + if got := s.Size(); got != 6 { + t.Fatalf("Size() = %d, want 6", got) + } + if s.tmpfile == nil { + t.Fatal("tmpfile was not created") + } + name := s.tmpfile.Name() + + buf := make([]byte, 3) + nn := 0 + nn, err = s.ReadAt(buf, 2) + if err != nil { + t.Fatalf("ReadAt() error = %v", err) + } + if nn != 3 || string(buf) != "cde" { + t.Fatalf("ReadAt() = (%d, %q), want (3, %q)", nn, string(buf), "cde") + } + + if err := s.Close(); err != nil { + t.Fatalf("Close() error = %v", err) + } + if _, err := os.Stat(name); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("temp file still exists or unexpected stat error: %v", err) + } + if got := s.Size(); got != 0 { + t.Fatalf("Size() after Close = %d, want 0", got) + } +} + +func TestLimitedStorePrimaryOnly(t *testing.T) { + s := NewLimitedStore(NewStoreMemory(), 8, nil) + + n, err := s.ReadFrom(bytes.NewBufferString("hello")) + if err != nil { + t.Fatalf("ReadFrom() error = %v", err) + } + if n != 5 { + t.Fatalf("ReadFrom() n = %d, want 5", n) + } + + buf := make([]byte, 5) + nn, err := s.ReadAt(buf, 0) + if err != nil { + t.Fatalf("ReadAt() error = %v", err) + } + if nn != 5 || string(buf) != "hello" { + t.Fatalf("ReadAt() = (%d, %q), want (5, %q)", nn, string(buf), "hello") + } + + if err := s.Close(); err != nil { + t.Fatalf("Close() error = %v", err) + } +} + +func TestLimitedStoreLimitReachedWithoutSecondary(t *testing.T) { + s := NewLimitedStore(NewStoreMemory(), 4, nil) + + n, err := s.ReadFrom(bytes.NewBufferString("hello")) + if !errors.Is(err, ErrStoreLimit) { + t.Fatalf("ReadFrom() error = %v, want %v", err, ErrStoreLimit) + } + if n != 4 { + t.Fatalf("ReadFrom() n = %d, want 4", n) + } +} + +func TestLimitedStoreFallsBackToSecondary(t *testing.T) { + s := NewLimitedStore(NewStoreMemory(), 4, NewStoreFile()) + + n, err := s.ReadFrom(bytes.NewBufferString("hello world")) + if err != nil { + t.Fatalf("ReadFrom() error = %v", err) + } + if n != 11 { + t.Fatalf("ReadFrom() n = %d, want 11", n) + } + + buf := make([]byte, 11) + nn, err := s.ReadAt(buf, 0) + if err != nil && !errors.Is(err, io.EOF) { + t.Fatalf("ReadAt() error = %v", err) + } + if nn != 11 || string(buf) != "hello world" { + t.Fatalf("ReadAt() = (%d, %q), want (11, %q)", nn, string(buf), "hello world") + } + + if err := s.Close(); err != nil { + t.Fatalf("Close() error = %v", err) + } +} + +func TestNewDefaultStore(t *testing.T) { + s := NewDefaultStore() + defer s.Close() + + n, err := s.ReadFrom(bytes.NewBufferString("abc")) + if err != nil { + t.Fatalf("ReadFrom() error = %v", err) + } + if n != 3 { + t.Fatalf("ReadFrom() n = %d, want 3", n) + } + + buf := make([]byte, 3) + nn, err := s.ReadAt(buf, 0) + if err != nil { + t.Fatalf("ReadAt() error = %v", err) + } + if nn != 3 || string(buf) != "abc" { + t.Fatalf("ReadAt() = (%d, %q), want (3, %q)", nn, string(buf), "abc") + } +}