Bound remote control object reads
This commit is contained in:
135
internal/adapters/storage/bounded_read_test.go
Normal file
135
internal/adapters/storage/bounded_read_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadObjectBoundedAcceptsExactLimitWithAbsentSizeMetadata(t *testing.T) {
|
||||
body := &trackingReadCloser{reader: bytes.NewReader([]byte("12345678")), chunkSize: 2}
|
||||
store := &boundedReadStore{read: func(context.Context, string) (ObjectInfo, io.ReadCloser, error) {
|
||||
return ObjectInfo{Key: "control.json", ETag: "generation"}, body, nil
|
||||
}}
|
||||
|
||||
info, data, err := ReadObjectBounded(context.Background(), store, "control.json", 8)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadObjectBounded() error = %v", err)
|
||||
}
|
||||
if string(data) != "12345678" || info.ETag != "generation" {
|
||||
t.Fatalf("ReadObjectBounded() = (%#v, %q), want opened object metadata and bytes", info, data)
|
||||
}
|
||||
if !body.closed {
|
||||
t.Fatal("object body was not closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadObjectBoundedRejectsLimitPlusOneDespiteMissingOrInaccurateMetadata(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
metadataSize int64
|
||||
}{
|
||||
{name: "missing", metadataSize: 0},
|
||||
{name: "inaccurate", metadataSize: 2},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
body := &trackingReadCloser{reader: bytes.NewReader([]byte("123456789")), chunkSize: 1}
|
||||
store := &boundedReadStore{read: func(context.Context, string) (ObjectInfo, io.ReadCloser, error) {
|
||||
return ObjectInfo{Key: "control.json", Size: test.metadataSize}, body, nil
|
||||
}}
|
||||
|
||||
_, data, err := ReadObjectBounded(context.Background(), store, "control.json", 8)
|
||||
var limitErr *ReadLimitError
|
||||
if !errors.As(err, &limitErr) {
|
||||
t.Fatalf("ReadObjectBounded() error = %v, want ReadLimitError", err)
|
||||
}
|
||||
if data != nil || body.bytesRead != 9 || !body.closed {
|
||||
t.Fatalf("data=%q bytes read=%d closed=%t, want nil, 9, true", data, body.bytesRead, body.closed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadObjectBoundedRejectsOversizedMetadataBeforeTransfer(t *testing.T) {
|
||||
body := &trackingReadCloser{reader: bytes.NewReader([]byte("small"))}
|
||||
store := &boundedReadStore{read: func(context.Context, string) (ObjectInfo, io.ReadCloser, error) {
|
||||
return ObjectInfo{Key: "control.json", Size: 9}, body, nil
|
||||
}}
|
||||
|
||||
_, _, err := ReadObjectBounded(context.Background(), store, "control.json", 8)
|
||||
var limitErr *ReadLimitError
|
||||
if !errors.As(err, &limitErr) {
|
||||
t.Fatalf("ReadObjectBounded() error = %v, want ReadLimitError", err)
|
||||
}
|
||||
if body.bytesRead != 0 || !body.closed {
|
||||
t.Fatalf("bytes read=%d closed=%t, want zero-byte transfer and closed body", body.bytesRead, body.closed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadObjectBoundedPropagatesCancellationAndClosesBody(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
body := &trackingReadCloser{reader: bytes.NewReader([]byte("12345678")), chunkSize: 1, afterRead: cancel}
|
||||
store := &boundedReadStore{read: func(context.Context, string) (ObjectInfo, io.ReadCloser, error) {
|
||||
return ObjectInfo{Key: "control.json"}, body, nil
|
||||
}}
|
||||
|
||||
_, data, err := ReadObjectBounded(ctx, store, "control.json", 8)
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("ReadObjectBounded() error = %v, want context cancellation", err)
|
||||
}
|
||||
if data != nil || body.bytesRead != 1 || !body.closed {
|
||||
t.Fatalf("data=%q bytes read=%d closed=%t, want nil, 1, true", data, body.bytesRead, body.closed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadObjectBoundedReturnsCloseFailure(t *testing.T) {
|
||||
closeErr := errors.New("close failed")
|
||||
body := &trackingReadCloser{reader: bytes.NewReader([]byte("ok")), closeErr: closeErr}
|
||||
store := &boundedReadStore{read: func(context.Context, string) (ObjectInfo, io.ReadCloser, error) {
|
||||
return ObjectInfo{Key: "control.json", Size: 2}, body, nil
|
||||
}}
|
||||
|
||||
_, data, err := ReadObjectBounded(context.Background(), store, "control.json", 8)
|
||||
if !errors.Is(err, closeErr) || data != nil || !body.closed {
|
||||
t.Fatalf("data=%q error=%v closed=%t, want close failure and no retained data", data, err, body.closed)
|
||||
}
|
||||
}
|
||||
|
||||
type boundedReadStore struct {
|
||||
ObjectStore
|
||||
read func(context.Context, string) (ObjectInfo, io.ReadCloser, error)
|
||||
}
|
||||
|
||||
func (s *boundedReadStore) Read(ctx context.Context, key string) (ObjectInfo, io.ReadCloser, error) {
|
||||
return s.read(ctx, key)
|
||||
}
|
||||
|
||||
type trackingReadCloser struct {
|
||||
reader io.Reader
|
||||
chunkSize int
|
||||
afterRead func()
|
||||
closeErr error
|
||||
bytesRead int
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (r *trackingReadCloser) Read(p []byte) (int, error) {
|
||||
if r.chunkSize > 0 && len(p) > r.chunkSize {
|
||||
p = p[:r.chunkSize]
|
||||
}
|
||||
n, err := r.reader.Read(p)
|
||||
r.bytesRead += n
|
||||
if n > 0 && r.afterRead != nil {
|
||||
r.afterRead()
|
||||
r.afterRead = nil
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *trackingReadCloser) Close() error {
|
||||
r.closed = true
|
||||
return r.closeErr
|
||||
}
|
||||
Reference in New Issue
Block a user