refactor(storage): read from local disk for putObject

This commit is contained in:
Damian Debkowski authored and Timothy Messier committed 2023-05-31 12:18:15 +00:00
1 parent 7263d08bc5
commit 39a07252b6
10 files changed
+471 -1213

No files matched your search

+2 -6
View File
@@ -99,10 +99,6 @@ func (tpc *WrappingPluginStorageClient) GetObject(ctx context.Context, req *plgp
return stream.client, nil
}
func (tpc *WrappingPluginStorageClient) PutObject(ctx context.Context, opts ...grpc.CallOption) (plgpb.StoragePluginService_PutObjectClient, error) {
stream := newPutObjectStream()
if err := tpc.Server.PutObject(stream.server); err != nil {
return nil, err
}
return stream.client, nil
func (tpc *WrappingPluginStorageClient) PutObject(ctx context.Context, req *plgpb.PutObjectRequest, opts ...grpc.CallOption) (*plgpb.PutObjectResponse, error) {
return tpc.Server.PutObject(ctx, req)
}
+3 -2
View File
@@ -52,8 +52,9 @@ func NewLoopbackPlugin(opt ...TestOption) (*LoopbackPlugin, error) {
hostMap: make(map[string][]*loopbackPluginHostInfo),
},
LoopbackStorage: &LoopbackStorage{
buckets: map[BucketName]Bucket{"default": {}},
errs: make([]PluginMockError, 0),
chunksSize: opts.withChunkSize,
buckets: map[BucketName]Bucket{"default": {}},
errs: make([]PluginMockError, 0),
},
}
+12 -1
View File
@@ -35,6 +35,7 @@ type TestOption func(*TestOptions) error
type TestOptions struct {
withMockBuckets map[BucketName]Bucket
withMockError []PluginMockError
withChunkSize int
}
// getTestOpts - iterate the inbound Options and return a struct
@@ -49,7 +50,9 @@ func getTestOpts(opt ...TestOption) (TestOptions, error) {
}
func getDefaultTestOptions() TestOptions {
return TestOptions{}
return TestOptions{
withChunkSize: 8,
}
}
// WithMockBuckets provides an option to create mocked external object store buckets.
@@ -67,3 +70,11 @@ func WithMockError(err PluginMockError) TestOption {
return nil
}
}
// WithChunkSize provides an option to set the chunkSize used for grpc streams.
func WithChunkSize(size int) TestOption {
return func(o *TestOptions) error {
o.withChunkSize = size
return nil
}
}
+109 -124
View File
@@ -9,6 +9,7 @@ import (
"crypto/sha256"
"fmt"
"io"
"os"
"strings"
"sync"
"time"
@@ -29,7 +30,7 @@ type TestPluginStorageServer struct {
ValidatePermissionsFn func(context.Context, *plgpb.ValidatePermissionsRequest) (*plgpb.ValidatePermissionsResponse, error)
HeadObjectFn func(context.Context, *plgpb.HeadObjectRequest) (*plgpb.HeadObjectResponse, error)
GetObjectFn func(*plgpb.GetObjectRequest, plgpb.StoragePluginService_GetObjectServer) error
PutObjectFn func(plgpb.StoragePluginService_PutObjectServer) error
PutObjectFn func(context.Context, *plgpb.PutObjectRequest) (*plgpb.PutObjectResponse, error)
plgpb.UnimplementedStoragePluginServiceServer
}
@@ -75,11 +76,11 @@ func (t TestPluginStorageServer) GetObject(req *plgpb.GetObjectRequest, stream p
return t.GetObjectFn(req, stream)
}
func (t TestPluginStorageServer) PutObject(stream plgpb.StoragePluginService_PutObjectServer) error {
func (t TestPluginStorageServer) PutObject(ctx context.Context, req *plgpb.PutObjectRequest) (*plgpb.PutObjectResponse, error) {
if t.PutObjectFn == nil {
return t.UnimplementedStoragePluginServiceServer.PutObject(stream)
return t.UnimplementedStoragePluginServiceServer.PutObject(ctx, req)
}
return t.PutObjectFn(stream)
return t.PutObjectFn(ctx, req)
}
type (
@@ -106,19 +107,20 @@ type storagePluginStorageInfo struct {
type LoopbackStorage struct {
m sync.Mutex
buckets map[BucketName]Bucket
errs []PluginMockError
chunksSize int
buckets map[BucketName]Bucket
errs []PluginMockError
}
func (l *LoopbackStorage) onCreateStorageBucket(ctx context.Context, req *plgpb.OnCreateStorageBucketRequest) (*plgpb.OnCreateStorageBucketResponse, error) {
const op = "loopback.(LoopbackPlugin).onCreateStorageBucket"
const op = "loopback.(LoopbackStorage).onCreateStorageBucket"
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: request is nil", op)
}
if req.Bucket == nil {
if req.GetBucket() == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing storage bucket", op)
}
if req.GetBucket().Secrets == nil {
if req.GetBucket().GetSecrets() == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing secrets", op)
}
l.m.Lock()
@@ -139,14 +141,14 @@ func (l *LoopbackStorage) onCreateStorageBucket(ctx context.Context, req *plgpb.
}
func (l *LoopbackStorage) onUpdateStorageBucket(ctx context.Context, req *plgpb.OnUpdateStorageBucketRequest) (*plgpb.OnUpdateStorageBucketResponse, error) {
const op = "loopback.(LoopbackPlugin).onUpdateStorageBucket"
const op = "loopback.(LoopbackStorage).onUpdateStorageBucket"
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: request is nil", op)
}
if req.NewBucket == nil {
if req.GetNewBucket() == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing storage bucket", op)
}
if req.GetNewBucket().Secrets == nil {
if req.GetNewBucket().GetSecrets() == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing secrets", op)
}
l.m.Lock()
@@ -167,28 +169,28 @@ func (l *LoopbackStorage) onUpdateStorageBucket(ctx context.Context, req *plgpb.
}
func (l *LoopbackStorage) onDeleteStorageBucket(ctx context.Context, req *plgpb.OnDeleteStorageBucketRequest) (*plgpb.OnDeleteStorageBucketResponse, error) {
const op = "loopback.(LoopbackPlugin).onDeleteStorageBucket"
const op = "loopback.(LoopbackStorage).onDeleteStorageBucket"
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: request is nil", op)
}
if req.Bucket == nil {
if req.GetBucket() == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing storage bucket", op)
}
if req.GetBucket().Secrets == nil {
if req.GetBucket().GetSecrets() == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing secrets", op)
}
return &plgpb.OnDeleteStorageBucketResponse{}, nil
}
func (l *LoopbackStorage) validatePermissions(ctx context.Context, req *plgpb.ValidatePermissionsRequest) (*plgpb.ValidatePermissionsResponse, error) {
const op = "loopback.(LoopbackPlugin).validatePermissions"
const op = "loopback.(LoopbackStorage).validatePermissions"
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: request is nil", op)
}
if req.Bucket == nil {
if req.GetBucket() == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing storage bucket", op)
}
if req.GetBucket().Secrets == nil {
if req.GetBucket().GetSecrets() == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing secrets", op)
}
l.m.Lock()
@@ -205,17 +207,17 @@ func (l *LoopbackStorage) validatePermissions(ctx context.Context, req *plgpb.Va
}
func (l *LoopbackStorage) headObject(ctx context.Context, req *plgpb.HeadObjectRequest) (*plgpb.HeadObjectResponse, error) {
const op = "loopback.(LoopbackPlugin).headObject"
const op = "loopback.(LoopbackStorage).headObject"
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: request is nil", op)
}
if req.Bucket == nil {
if req.GetBucket() == nil {
return nil, status.Errorf(codes.InvalidArgument, "$%s: missing storage bucket", op)
}
if req.GetBucket().Secrets == nil {
if req.GetBucket().GetSecrets() == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing secrets", op)
}
if req.Key == "" {
if req.GetKey() == "" {
return nil, status.Errorf(codes.InvalidArgument, "%s; missing object key", op)
}
l.m.Lock()
@@ -245,17 +247,17 @@ func (l *LoopbackStorage) headObject(ctx context.Context, req *plgpb.HeadObjectR
}
func (l *LoopbackStorage) getObject(req *plgpb.GetObjectRequest, stream plgpb.StoragePluginService_GetObjectServer) error {
const op = "loopback.(LoopbackPlugin).getObject"
const op = "loopback.(LoopbackStorage).getObject"
if req == nil {
return status.Errorf(codes.InvalidArgument, "%s: request is nil", op)
}
if req.Bucket == nil {
if req.GetBucket() == nil {
return status.Errorf(codes.InvalidArgument, "%s: missing storage bucket", op)
}
if req.GetBucket().Secrets == nil {
if req.GetBucket().GetSecrets() == nil {
return status.Errorf(codes.InvalidArgument, "%s: missing secrets", op)
}
if req.Key == "" {
if req.GetKey() == "" {
return status.Errorf(codes.InvalidArgument, "%s; missing object key", op)
}
l.m.Lock()
@@ -288,110 +290,93 @@ func (l *LoopbackStorage) getObject(req *plgpb.GetObjectRequest, stream plgpb.St
return nil
}
func (l *LoopbackStorage) putObject(stream plgpb.StoragePluginService_PutObjectServer) error {
const op = "loopback.(LoopbackPlugin).putObject"
if stream == nil {
return status.Errorf(codes.Internal, "%s: missing stream", op)
func (l *LoopbackStorage) putObject(ctx context.Context, req *plgpb.PutObjectRequest) (*plgpb.PutObjectResponse, error) {
const op = "loopback.(LoopbackStorage).putObject"
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: request is nil", op)
}
go func() {
var (
request *plgpb.PutObjectRequest_Request
objectChunks []Chunk
objectData []byte
)
for {
req, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
_ = stream.SendMsg(status.Errorf(codes.Internal, "%s: failed to recv message from stream: %v", op, err))
return
}
switch data := req.Data.(type) {
case *plgpb.PutObjectRequest_Request:
if data.Request == nil {
_ = stream.SendMsg(status.Errorf(codes.InvalidArgument, "%s: missing request metadata", op))
return
}
if data.Request.GetBucket() == nil {
_ = stream.SendMsg(status.Errorf(codes.InvalidArgument, "%s: missing storage bucket", op))
return
}
if data.Request.GetBucket().GetSecrets() == nil {
_ = stream.SendMsg(status.Errorf(codes.InvalidArgument, "%s: missing secrets", op))
return
}
if data.Request.Key == "" {
_ = stream.SendMsg(status.Errorf(codes.InvalidArgument, "%s; missing object key", op))
return
}
request = data
case *plgpb.PutObjectRequest_FileChunk:
objectChunks = append(objectChunks, data.FileChunk)
objectData = append(objectData, data.FileChunk...)
case nil:
continue
default:
_ = stream.SendMsg(status.Errorf(codes.Internal, "%s: unknown message type: %v", op, data))
return
}
if req.GetBucket() == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing storage bucket", op)
}
if req.GetBucket().GetBucketName() == "" {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing bucket name", op)
}
if req.GetBucket().GetSecrets() == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing secrets", op)
}
if req.GetKey() == "" {
return nil, status.Errorf(codes.InvalidArgument, "%s; missing object key", op)
}
if req.GetPath() == "" {
return nil, status.Errorf(codes.InvalidArgument, "%s; missing path", op)
}
info, err := os.Stat(req.Path)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: file check failed: %v", op, err)
}
if info == nil {
return nil, status.Errorf(codes.Internal, "%s: failed to get file info", op)
}
if info.IsDir() {
return nil, status.Errorf(codes.InvalidArgument, "%s: path is a directory", op)
}
l.m.Lock()
defer l.m.Unlock()
bucket, ok := l.buckets[BucketName(req.GetBucket().GetBucketName())]
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "%s: bucket not found", op)
}
for _, err := range l.errs {
if err.match(req.GetBucket(), req.GetKey()) {
return nil, status.Errorf(err.errCode, "%s: %s", op, err.errMsg)
}
if request == nil {
_ = stream.SendMsg(status.Errorf(codes.InvalidArgument, "%s: request is nil", op))
return
}
if len(objectData) <= 0 {
_ = stream.SendMsg(status.Errorf(codes.InvalidArgument, "%s: missing object data", op))
return
}
l.m.Lock()
defer l.m.Unlock()
bucket, ok := l.buckets[BucketName(request.Request.GetBucket().GetBucketName())]
if !ok {
_ = stream.SendMsg(status.Errorf(codes.NotFound, "%s: bucket not found", op))
return
}
for _, err := range l.errs {
if err.match(request.Request.GetBucket(), request.Request.GetKey()) {
_ = stream.SendMsg(status.Errorf(err.errCode, "%s: %s", op, err.errMsg))
return
}
}
hash := sha256.New()
if _, err := io.Copy(hash, bytes.NewReader(objectData)); err != nil {
_ = stream.SendMsg(status.Errorf(codes.Internal, "%s: failed to hash object: %v", op, err))
return
}
contentLength := int64(len(objectData))
lastModified := time.Now()
}
// Ensure all the directories within the object key exist
parts := strings.Split(request.Request.GetKey(), "/")
tempPath := request.Request.GetBucket().GetBucketPrefix()
for _, p := range parts[:len(parts)-1] {
// Directories should have trailing `/` in the key
tempPath = fmt.Sprintf("%v%v/", tempPath, p)
bucket[ObjectName(tempPath)] = &storagePluginStorageInfo{
lastModified: &lastModified,
}
}
lastModified := time.Now()
objectData, err := os.ReadFile(req.Path)
if err != nil {
return nil, status.Errorf(codes.Internal, "%s: failed to read file", op)
}
if len(objectData) == 0 {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing object data", op)
}
contentLength := int64(len(objectData))
// Now insert the object
objectPath := ObjectName(request.Request.GetBucket().GetBucketPrefix() + request.Request.GetKey())
bucket[objectPath] = &storagePluginStorageInfo{
DataChunks: objectChunks,
contentLength: &contentLength,
lastModified: &lastModified,
parts := strings.Split(req.GetKey(), "/")
tempPath := req.GetBucket().GetBucketPrefix()
for _, p := range parts[:len(parts)-1] {
// Directories should have trailing `/` in the key
tempPath = fmt.Sprintf("%v%v/", tempPath, p)
bucket[ObjectName(tempPath)] = &storagePluginStorageInfo{
lastModified: &lastModified,
}
if err := stream.SendAndClose(&plgpb.PutObjectResponse{
ChecksumSha_256: hash.Sum(nil),
}); err != nil {
_ = stream.SendMsg(status.Errorf(codes.Internal, "%s: failed to close stream: %v", op, err))
return
}
objectChunks := []Chunk{}
for i := 0; i < len(objectData); i = i + l.chunksSize {
j := i + l.chunksSize
if j > len(objectData) {
j = len(objectData)
}
}()
return nil
objectChunks = append(objectChunks, copyBytes(objectData[i:j]))
}
// Now insert the object
objectPath := ObjectName(req.GetBucket().GetBucketPrefix() + req.GetKey())
bucket[objectPath] = &storagePluginStorageInfo{
DataChunks: objectChunks,
contentLength: &contentLength,
lastModified: &lastModified,
}
hash := sha256.New()
if _, err := io.Copy(hash, bytes.NewReader(objectData)); err != nil {
return nil, status.Errorf(codes.Internal, "%s: failed to hash object: %v", op, err)
}
return &plgpb.PutObjectResponse{
ChecksumSha_256: hash.Sum(nil),
}, nil
}
func MockObject(data []Chunk) *storagePluginStorageInfo {
+185 -182
View File
@@ -8,6 +8,10 @@ import (
"context"
"crypto/sha256"
"io"
"io/fs"
"os"
"path"
"strings"
"sync"
"testing"
@@ -666,62 +670,68 @@ func TestLoopbackGetObject(t *testing.T) {
}
func TestLoopbackPutObject(t *testing.T) {
require, assert := tr.New(t), ta.New(t)
require := tr.New(t)
td := t.TempDir()
mockStorageMapData := map[BucketName]Bucket{
"aws_s3_mock": {},
"aws_s3_err": {},
"object_store": {},
"object_store_err": {},
}
plg, err := NewLoopbackPlugin(
WithMockBuckets(mockStorageMapData),
WithMockError(PluginMockError{
bucketName: "aws_s3_err",
bucketName: "object_store_err",
objectKey: "mock_object",
errMsg: "invalid credentials",
errCode: codes.PermissionDenied,
}),
)
assert.NoError(err)
require.NoError(err)
require.NotNil(plg)
client := NewWrappingPluginStorageClient(plg)
secrets := &structpb.Struct{
Fields: map[string]*structpb.Value{
"AWS_ACCESS_KEY_ID": structpb.NewStringValue("access_key_id"),
"AWS_SECRET_ACCESS_KEY": structpb.NewStringValue("secret_access_key"),
},
}
tests := []struct {
name string
request *plgpb.PutObjectRequest
dataChunks []Chunk
expectedErr codes.Code
name string
request *plgpb.PutObjectRequest
file *os.File
expectedErr codes.Code
expectedContent string
}{
{
name: "missing request metadata",
name: "missing-request",
expectedErr: codes.InvalidArgument,
},
{
name: "missing-bucket",
request: &plgpb.PutObjectRequest{},
expectedErr: codes.InvalidArgument,
},
{
name: "missing-bucket-name",
request: &plgpb.PutObjectRequest{
Data: &plgpb.PutObjectRequest_Request{},
Bucket: &storagebuckets.StorageBucket{},
},
expectedErr: codes.InvalidArgument,
},
{
name: "missing storage bucket",
name: "missing-secrets",
request: &plgpb.PutObjectRequest{
Data: &plgpb.PutObjectRequest_Request{
Request: &plgpb.PutObjectMetadata{},
Bucket: &storagebuckets.StorageBucket{
BucketName: "object_store",
},
},
expectedErr: codes.InvalidArgument,
},
{
name: "missing secrets",
name: "missing-key",
request: &plgpb.PutObjectRequest{
Data: &plgpb.PutObjectRequest_Request{
Request: &plgpb.PutObjectMetadata{
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Bucket: &storagebuckets.StorageBucket{
BucketName: "object_store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
"SECRET_KEY_ID": structpb.NewStringValue("secret_key_id"),
},
},
},
@@ -729,210 +739,210 @@ func TestLoopbackPutObject(t *testing.T) {
expectedErr: codes.InvalidArgument,
},
{
name: "missing object key",
name: "missing-path",
request: &plgpb.PutObjectRequest{
Data: &plgpb.PutObjectRequest_Request{
Request: &plgpb.PutObjectMetadata{
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Bucket: &storagebuckets.StorageBucket{
BucketName: "object_store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
"SECRET_KEY_ID": structpb.NewStringValue("secret_key_id"),
},
},
},
Key: "test-file",
},
expectedErr: codes.InvalidArgument,
},
{
name: "empty object data",
dataChunks: []Chunk{
[]byte(""),
},
name: "file-not-found",
request: &plgpb.PutObjectRequest{
Data: &plgpb.PutObjectRequest_Request{
Request: &plgpb.PutObjectMetadata{
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Bucket: &storagebuckets.StorageBucket{
BucketName: "object_store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
"SECRET_KEY_ID": structpb.NewStringValue("secret_key_id"),
},
Key: "mock_object",
},
},
Key: "test-file",
Path: path.Join(td, "test-file"),
},
expectedErr: codes.InvalidArgument,
},
{
name: "bucket not found",
dataChunks: []Chunk{
[]byte("THIS IS A MOCKED OBJECT"),
},
name: "path-is-directory",
request: &plgpb.PutObjectRequest{
Data: &plgpb.PutObjectRequest_Request{
Request: &plgpb.PutObjectMetadata{
Bucket: &storagebuckets.StorageBucket{
BucketName: "invalid_bucket",
Secrets: secrets,
Bucket: &storagebuckets.StorageBucket{
BucketName: "object_store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
"SECRET_KEY_ID": structpb.NewStringValue("secret_key_id"),
},
Key: "mock_object",
},
},
Key: "test-directory",
Path: path.Join(td, "test-directory"),
},
expectedErr: codes.NotFound,
file: func() *os.File {
err := os.Mkdir(path.Join(td, "test-directory"), fs.ModeAppend)
require.NoError(err)
return nil
}(),
expectedErr: codes.InvalidArgument,
},
{
name: "mock error",
dataChunks: []Chunk{
[]byte("THIS IS A MOCKED OBJECT"),
},
name: "bucket-not-found",
request: &plgpb.PutObjectRequest{
Data: &plgpb.PutObjectRequest_Request{
Request: &plgpb.PutObjectMetadata{
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_err",
Secrets: secrets,
Bucket: &storagebuckets.StorageBucket{
BucketName: "object_store_dne",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
"SECRET_KEY_ID": structpb.NewStringValue("secret_key_id"),
},
Key: "mock_object",
},
},
Key: "test-bucket-not-found",
Path: path.Join(td, "test-bucket-not-found"),
},
file: func() *os.File {
file, err := os.Create(path.Join(td, "test-bucket-not-found"))
require.NoError(err)
return file
}(),
expectedErr: codes.InvalidArgument,
},
{
name: "mock-error",
request: &plgpb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "object_store_err",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
"SECRET_KEY_ID": structpb.NewStringValue("secret_key_id"),
},
},
},
Key: "mock_object",
Path: path.Join(td, "test-bucket-not-found"),
},
file: func() *os.File {
file, err := os.Create(path.Join(td, "test-mock-error"))
require.NoError(err)
return file
}(),
expectedErr: codes.PermissionDenied,
},
{
name: "valid object",
dataChunks: []Chunk{
[]byte("THIS IS A MOCKED OBJECT"),
},
name: "emtpy-object-data",
request: &plgpb.PutObjectRequest{
Data: &plgpb.PutObjectRequest_Request{
Request: &plgpb.PutObjectMetadata{
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Bucket: &storagebuckets.StorageBucket{
BucketName: "object_store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
"SECRET_KEY_ID": structpb.NewStringValue("secret_key_id"),
},
Key: "mock_object",
},
},
Key: "test-empty-object-data",
Path: path.Join(td, "test-empty-object-data"),
},
file: func() *os.File {
file, err := os.Create(path.Join(td, "test-empty-object-data"))
require.NoError(err)
require.NoError(file.Close())
return file
}(),
expectedErr: codes.InvalidArgument,
},
{
name: "valid object with dir in key",
dataChunks: []Chunk{
[]byte("THIS IS A MOCKED OBJECT"),
},
request: &plgpb.PutObjectRequest{
Data: &plgpb.PutObjectRequest_Request{
Request: &plgpb.PutObjectMetadata{
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Bucket: &storagebuckets.StorageBucket{
BucketName: "object_store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
"SECRET_KEY_ID": structpb.NewStringValue("secret_key_id"),
},
Key: "foo/bar/zoo/mocked_object",
},
},
Key: "foo/bar/zoo/mocked_object",
Path: path.Join(td, "test-valid-object-dir-in-key"),
},
file: func() *os.File {
file, err := os.Create(path.Join(td, "test-valid-object-dir-in-key"))
require.NoError(err)
n, err := file.WriteString("TEST OBJ WITH DIR IN KEY!")
require.NoError(err)
require.Equal(len("TEST OBJ WITH DIR IN KEY!"), n)
require.NoError(file.Close())
return file
}(),
expectedContent: "TEST OBJ WITH DIR IN KEY!",
},
{
name: "valid object w/ prefix",
dataChunks: []Chunk{
[]byte("THIS IS A MOCKED OBJECT"),
},
request: &plgpb.PutObjectRequest{
Data: &plgpb.PutObjectRequest_Request{
Request: &plgpb.PutObjectMetadata{
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
BucketPrefix: "/filtered/",
Secrets: secrets,
Bucket: &storagebuckets.StorageBucket{
BucketName: "object_store",
BucketPrefix: "/filtered/",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
"SECRET_KEY_ID": structpb.NewStringValue("secret_key_id"),
},
Key: "mock_object",
},
},
},
},
{
name: "valid object w/ multiple chunks",
dataChunks: []Chunk{
[]byte("PART A: 1234567890"),
[]byte("PART B: 0987654321"),
[]byte("PART C: qwertyuiop"),
[]byte("PART D: poiuytrewq"),
[]byte("PART E: asdfghjkl"),
[]byte("PART F: lkjhgfdsa"),
[]byte("PART G: zxcvbnm"),
[]byte("PART H: mnbvcxz"),
},
request: &plgpb.PutObjectRequest{
Data: &plgpb.PutObjectRequest_Request{
Request: &plgpb.PutObjectMetadata{
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
},
Key: "multi_chunk_object",
},
},
Key: "mocked_object",
Path: path.Join(td, "test-valid-object-prefix"),
},
file: func() *os.File {
file, err := os.Create(path.Join(td, "test-valid-object-prefix"))
require.NoError(err)
n, err := file.WriteString("TEST OBJ WITH PREFIX!")
require.NoError(err)
require.Equal(len("TEST OBJ WITH PREFIX!"), n)
require.NoError(file.Close())
return file
}(),
expectedContent: "TEST OBJ WITH PREFIX!",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stream, err := client.PutObject(context.Background())
assert.NoError(err)
assert.NotNil(stream)
var objectData []byte
var closeResponse *plgpb.PutObjectResponse
var closeErr error
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
err = stream.Send(tt.request)
assert.NoError(err)
for _, chunk := range tt.dataChunks {
objectData = append(objectData, chunk...)
err = stream.Send(&plgpb.PutObjectRequest{
Data: &plgpb.PutObjectRequest_FileChunk{
FileChunk: chunk,
},
})
assert.NoError(err)
}
closeResponse, closeErr = stream.CloseAndRecv()
}()
wg.Wait()
resp, err := client.PutObject(context.Background(), tt.request)
if tt.expectedErr != codes.OK {
assert.Error(closeErr)
assert.Nil(closeResponse)
s, ok := status.FromError(closeErr)
require.Error(err)
require.Nil(resp)
s, ok := status.FromError(err)
require.True(ok, "invalid error type")
assert.Equal(s.Code(), tt.expectedErr)
require.Equal(s.Code(), tt.expectedErr)
return
}
require.NoError(err)
require.NotNil(resp)
assert.NoError(closeErr)
require.NotNil(closeResponse)
var actualObject *storagePluginStorageInfo
objectPath := ObjectName(tt.request.GetRequest().GetBucket().GetBucketPrefix() + tt.request.GetRequest().GetKey())
if obj, ok := mockStorageMapData[BucketName(tt.request.GetRequest().GetBucket().GetBucketName())][objectPath]; ok {
actualObject = obj
actualBucket, ok := plg.buckets[BucketName(tt.request.Bucket.BucketName)]
require.True(ok)
require.NotEmpty(actualBucket)
actualObject, ok := actualBucket[ObjectName(path.Join(tt.request.Bucket.BucketPrefix, tt.request.Key))]
require.True(ok)
require.NotEmpty(actualObject)
actualData := []byte{}
for _, c := range actualObject.DataChunks {
actualData = append(actualData, c...)
}
require.NotNil(actualObject)
assert.EqualValues(tt.dataChunks, actualObject.DataChunks)
assert.NotEmpty(closeResponse.ChecksumSha_256)
require.Equal(tt.expectedContent, string(actualData))
hash := sha256.New()
_, err = io.Copy(hash, bytes.NewReader(objectData))
_, err = io.Copy(hash, bytes.NewReader(actualData))
require.NoError(err)
assert.Equal(hash.Sum(nil), closeResponse.ChecksumSha_256)
require.ElementsMatch(hash.Sum(nil), resp.ChecksumSha_256)
})
}
}
func TestLoopbackStoragePlugin(t *testing.T) {
td := t.TempDir()
require, assert := tr.New(t), ta.New(t)
mockStorageMapData := map[BucketName]Bucket{
@@ -951,38 +961,31 @@ func TestLoopbackStoragePlugin(t *testing.T) {
},
}
objectData := "THIS IS A MOCKED OBJECT"
bucket := &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
BucketPrefix: "/filtered/path/",
Secrets: secrets,
}
objectData := []byte("THIS IS A MOCKED OBJECT")
putObjectStream, err := client.PutObject(context.Background())
assert.NoError(err)
err = putObjectStream.Send(&plgpb.PutObjectRequest{
Data: &plgpb.PutObjectRequest_Request{
Request: &plgpb.PutObjectMetadata{
Bucket: bucket,
Key: "dir1/mock_object",
},
},
})
assert.NoError(err)
err = putObjectStream.Send(&plgpb.PutObjectRequest{
Data: &plgpb.PutObjectRequest_FileChunk{
FileChunk: objectData,
},
})
assert.NoError(err)
putResponse, err := putObjectStream.CloseAndRecv()
file, err := os.Create(path.Join(td, "test-put-object"))
require.NoError(err)
n, err := file.WriteString(objectData)
require.NoError(err)
require.Equal(len(objectData), n)
require.NoError(file.Close())
putResponse, err := client.PutObject(
context.Background(), &plgpb.PutObjectRequest{
Bucket: bucket,
Key: "dir1/mock_object",
Path: path.Join(td, "test-put-object"),
})
assert.NoError(err)
require.NotNil(putResponse)
assert.NotEmpty(putResponse.GetChecksumSha_256())
hash := sha256.New()
_, err = io.Copy(hash, bytes.NewReader(objectData))
_, err = io.Copy(hash, strings.NewReader(objectData))
require.NoError(err)
assert.EqualValues(hash.Sum(nil), putResponse.GetChecksumSha_256())
@@ -229,274 +229,3 @@ func newGetObjectStream() *getObjectStream {
}
return stream
}
// putObjectStreamRequest is used to mock a message sent from the client to the server.
type putObjectStreamRequest struct {
msg *plgpb.PutObjectRequest
err error
}
// puObjectStreamResponse is used to mock a message sent from the server to the client.
type putObjectStreamResponse struct {
msg *plgpb.PutObjectResponse
err error
}
// putObjectStream is used to mock the interactions between
// the client and server for the PutObject method.
type putObjectStream struct {
client plgpb.StoragePluginService_PutObjectClient
server plgpb.StoragePluginService_PutObjectServer
// requests is used to mock the client sending messages to the server.
requests chan *putObjectStreamRequest
// responses is used to mock the server sending messages to the client.
responses chan *putObjectStreamResponse
m *sync.Mutex
clientClosed bool
serverClosed bool
}
func (s *putObjectStream) CloseClient() {
s.m.Lock()
defer s.m.Unlock()
if s.clientClosed {
return
}
close(s.requests)
s.clientClosed = true
}
func (s *putObjectStream) IsClientClosed() bool {
s.m.Lock()
defer s.m.Unlock()
return s.clientClosed
}
func (s *putObjectStream) CloseServer() {
s.m.Lock()
defer s.m.Unlock()
if s.serverClosed {
return
}
close(s.responses)
s.serverClosed = true
}
func (s *putObjectStream) IsServerClosed() bool {
s.m.Lock()
defer s.m.Unlock()
return s.serverClosed
}
// putObjectClient is used to mock the client stream
// interactions for the PutObject method.
type putObjectClient struct {
sendToServer chan *putObjectStreamRequest
sentFromServer chan *putObjectStreamResponse
// closeStream is used to close the channels of the stream.
// This is used to prevent the channels from being closed multiple times.
// This is needed because the channel can be closed by the client or the server.
closeStream func()
// isStreamClosed is used to check if the stream is closed.
// This is needed because the channel can be closed by the client or the server.
isStreamClosed func() bool
}
// Send will send a message to the server.
// Send will return an error if the request is nil.
// Send will return an error if the stream is closed.
func (c *putObjectClient) Send(req *plgpb.PutObjectRequest) error {
if req == nil {
return fmt.Errorf(`parameter arg "req PutObjectRequest" cannot be nil`)
}
if c.isStreamClosed() {
return fmt.Errorf("stream is closed")
}
c.sendToServer <- &putObjectStreamRequest{msg: req}
return nil
}
// CloseAndRecv will return an PutObjectResponse if the server returns one.
// CloseAndRecv will return an error if the server returns an error.
// CloseAndRecv will return an io.EOF error if the server closes the stream.
// CloseAndRecv will close the stream used to send messages to the server.
func (c *putObjectClient) CloseAndRecv() (*plgpb.PutObjectResponse, error) {
c.closeStream()
resp, ok := <-c.sentFromServer
if !ok {
return nil, io.EOF
}
return resp.msg, resp.err
}
// Header should not be used.
// Header is implemeted to satisfy the grpc.ClientStream interface.
// Header will always return an empty metadata and an nil error.
func (c *putObjectClient) Header() (metadata.MD, error) {
return make(metadata.MD), nil
}
// Trailer should not be used.
// Trailer is implemeted to satisfy the grpc.ClientStream interface.
// Trailer will always return an empty metadata.
func (c *putObjectClient) Trailer() metadata.MD {
return make(metadata.MD)
}
// CloseSend will close the channel used to retrieve messages from
// the server. This will cause the CloseAndRecv method to return io.EOF.
// CloseSend will always return a nill error.
func (c *putObjectClient) CloseSend() error {
if c.isStreamClosed() {
return fmt.Errorf("stream is closed")
}
c.closeStream()
return nil
}
// Context will always return a Background context.
func (c *putObjectClient) Context() context.Context {
return context.Background()
}
// SendMsg should not be used.
// SendMsg is implemeted to satisfy the grpc.ClientStream interface.
// SendMsg will always return an nil error.
func (c *putObjectClient) SendMsg(m interface{}) error {
return nil
}
// RecvMsg should not be used.
// RecvMsg is implemeted to satisfy the grpc.ClientStream interface.
// RecvMsg will always return an nil error.
func (c *putObjectClient) RecvMsg(m interface{}) error {
return nil
}
// putObjectServer is used to mock the server stream
// interactions for the PutObject method.
type putObjectServer struct {
sentFromClient chan *putObjectStreamRequest
sentToClient chan *putObjectStreamResponse
// closeStream is used to close the channels of the stream.
// This is used to prevent the channels from being closed multiple times.
// This is needed because the channel can be closed by the client or the server.
closeStream func()
// isStreamClosed is used to check if the stream is closed.
// This is needed because the channel can be closed by the client or the server.
isStreamClosed func() bool
}
// SendAndClose will send a PutObjectResponse to the client.
// SendAndClose will return an error if the response is nil.
// SendAndClose will close the stream used to send messages from the client.
// SendAndClose will return an error if the stream is closed.
func (s *putObjectServer) SendAndClose(resp *plgpb.PutObjectResponse) error {
if resp == nil {
return fmt.Errorf(`parameter arg "resp PutObjectResponse" cannot be nil`)
}
if s.isStreamClosed() {
return fmt.Errorf("stream is closed")
}
s.sentToClient <- &putObjectStreamResponse{msg: resp}
s.closeStream()
return nil
}
// Recv will read a PutObjectRequest from the stream.
// Recv will return an io.EOF error if the stream is closed.
func (s *putObjectServer) Recv() (*plgpb.PutObjectRequest, error) {
req, ok := <-s.sentFromClient
if !ok {
return nil, io.EOF
}
return req.msg, req.err
}
// SetHeader should not be used.
// SetHeader is implemeted to satisfy the grpc.ServerStream interface.
// SetHeader will always return an nil error.
func (s *putObjectServer) SetHeader(metadata.MD) error {
return nil
}
// SendHeader should not be used.
// SendHeader is implemeted to satisfy the grpc.ServerStream interface.
// SendHeader will always return an nil error.
func (s *putObjectServer) SendHeader(metadata.MD) error {
return nil
}
// SetTrailer should not be used.
// SetTrailer is implemeted to satisfy the grpc.ServerStream interface.
func (s *putObjectServer) SetTrailer(metadata.MD) {
}
// Context will always return a Background context.
func (s *putObjectServer) Context() context.Context {
return context.Background()
}
// SendMsg allows sending errors other than io.EOF to the client.
// Sending an error message will close the stream.
// SendMsg allows sending PutObjectResponse messages to the client.
// SendMsg returns an invalid argument error if the message is not
// an error or PutObjectResponse.
// SendMsg will return an error if the stream is closed.
func (s *putObjectServer) SendMsg(m interface{}) error {
switch msg := m.(type) {
case *plgpb.PutObjectResponse:
if s.isStreamClosed() {
return fmt.Errorf("stream is closed")
}
s.sentToClient <- &putObjectStreamResponse{msg: msg}
case error:
if s.isStreamClosed() {
return fmt.Errorf("stream is closed")
}
s.sentToClient <- &putObjectStreamResponse{err: msg}
defer s.closeStream()
default:
return fmt.Errorf("invalid argument %v", m)
}
return nil
}
// RecvMsg should not be used.
// RecvMsg is implemeted to satisfy the grpc.ServerStream interface.
// RecvMsg will always return an nil error.
func (s *putObjectServer) RecvMsg(m interface{}) error {
return nil
}
// newPutObjectStream will create a mock stream for the PutObject method.
// The client and server stream is mocked by creating a PutObjectRequest,
// PutObjectResponse and an error channel that is shared between the client
// and server.
func newPutObjectStream() *putObjectStream {
stream := &putObjectStream{
m: new(sync.Mutex),
requests: make(chan *putObjectStreamRequest),
responses: make(chan *putObjectStreamResponse),
}
stream.client = &putObjectClient{
sendToServer: stream.requests,
sentFromServer: stream.responses,
closeStream: stream.CloseClient,
isStreamClosed: stream.IsClientClosed,
}
stream.server = &putObjectServer{
sentFromClient: stream.requests,
sentToClient: stream.responses,
closeStream: stream.CloseServer,
isStreamClosed: stream.IsServerClosed,
}
return stream
}
@@ -227,242 +227,6 @@ func TestNewGetObjectStream(t *testing.T) {
require.False(ok)
}
func Test_PutObjectStream_Client(t *testing.T) {
require := tr.New(t)
stream := newPutObjectStream()
require.NotNil(stream)
require.NotNil(stream.client)
// Validate Header is not nil and does not return an error
header, err := stream.client.Header()
require.NotNil(header)
require.NoError(err)
// Validate Trailer is not nil
trailer := stream.client.Trailer()
require.NotNil(trailer)
// Validate Context is not nil
ctx := stream.client.Context()
require.NotNil(ctx)
// Validate SendMsg does not write to the channel requests
err = stream.client.SendMsg(nil)
require.NoError(err)
require.Empty(stream.requests)
// Validate RecvMsg does not read from the channel requests
streamSize := len(stream.requests)
var msg *putObjectStreamResponse
err = stream.client.RecvMsg(msg)
require.NoError(err)
require.Nil(msg)
require.Equal(streamSize, len(stream.requests))
// Validate Send returns an error when the request is nil
err = stream.client.Send(nil)
require.Error(err)
// Validate Send writes to the channel requests
var wg sync.WaitGroup
go func() {
err = stream.client.Send(&plgpb.PutObjectRequest{})
require.NoError(err)
}()
wg.Add(1)
go func() {
defer wg.Done()
req, ok := <-stream.requests
require.True(ok)
require.NotNil(req)
require.NotNil(req.msg)
require.Nil(req.err)
}()
wg.Wait()
// Validate CloseSend does not return an error
// Validate clientClosed is set to true
// Validate the channel requests is closed
var ok bool
err = stream.client.CloseSend()
require.NoError(err)
require.True(stream.clientClosed)
_, ok = <-stream.requests
require.False(ok)
// Validate CloseSend does return an error when the channel is closed
// Validate clientClosed is set to true
// Validate the channel requests is closed
err = stream.client.CloseSend()
require.Error(err)
require.Contains(err.Error(), "stream is closed")
require.True(stream.clientClosed)
_, ok = <-stream.requests
require.False(ok)
// Validate Send returns an error when the channel is closed
err = stream.client.Send(&plgpb.PutObjectRequest{})
require.Error(err)
require.Contains(err.Error(), "stream is closed")
require.Empty(stream.requests)
// Reset stream
stream = newPutObjectStream()
// Validate CloseAndRecv returns an EOF error when the channel responses is closed
// Validate CloseAndRecv closes the channel requests
stream.CloseServer()
resp, err := stream.client.CloseAndRecv()
require.Equal(io.EOF, err)
require.Nil(resp)
require.True(stream.clientClosed)
require.True(stream.IsClientClosed())
require.True(stream.serverClosed)
require.True(stream.IsServerClosed())
}
func Test_PutObjectStream_Server(t *testing.T) {
require := tr.New(t)
stream := newPutObjectStream()
require.NotNil(stream)
require.NotNil(stream.server)
// Validate SetHeader does not return an error
err := stream.server.SetHeader(nil)
require.NoError(err)
// Validate SendHeader does not return an error
err = stream.server.SendHeader(nil)
require.NoError(err)
// Validate Context returns a context
ctx := stream.server.Context()
require.NotNil(ctx)
// Validate RecvMsg does not read from the channel requests
streamSize := len(stream.requests)
var msg *putObjectStreamRequest
err = stream.server.RecvMsg(msg)
require.NoError(err)
require.Nil(msg)
require.Equal(streamSize, len(stream.requests))
// Validate SendMsg returns an error for unknown message types
err = stream.server.SendMsg(map[string]string{})
require.Error(err)
require.Contains(err.Error(), "invalid argument")
// Validate SendMsg does write a message to the channel responses
var wg sync.WaitGroup
go func() {
err = stream.server.SendMsg(&plgpb.PutObjectResponse{})
require.NoError(err)
}()
wg.Add(1)
go func() {
defer wg.Done()
resp, ok := <-stream.responses
require.True(ok)
require.NotNil(resp)
require.NotNil(resp.msg)
require.Nil(resp.err)
}()
wg.Wait()
// Validate SendMsg does write a error to the channel responses
// Validate SendMsg closes the channel responses
go func() {
err = stream.server.SendMsg(fmt.Errorf("mock error"))
require.NoError(err)
}()
wg.Add(1)
go func() {
defer wg.Done()
resp, ok := <-stream.responses
require.True(ok)
require.NotNil(resp)
require.NotNil(resp.err)
require.Nil(resp.msg)
}()
wg.Wait()
_, ok := <-stream.responses
require.False(ok)
require.True(stream.serverClosed)
require.True(stream.IsServerClosed())
// Validate SendMsg does return an error when the channel responses is closed
err = stream.server.SendMsg(&plgpb.PutObjectResponse{})
require.Error(err)
require.Contains(err.Error(), "stream is closed")
err = stream.server.SendMsg(fmt.Errorf("mock error"))
require.Error(err)
require.Contains(err.Error(), "stream is closed")
// Validate SendAndClose returns an error when response is nil
err = stream.server.SendAndClose(nil)
require.Error(err)
// Validate SendAndClose returns an error when the channel responses is closed
err = stream.server.SendAndClose(&plgpb.PutObjectResponse{})
require.Error(err)
require.Contains(err.Error(), "stream is closed")
// Reset stream
stream = newPutObjectStream()
// Validate SendAndClose closes the responses channel
go func() {
err = stream.server.SendAndClose(&plgpb.PutObjectResponse{})
require.NoError(err)
}()
wg.Add(1)
go func() {
defer wg.Done()
resp, ok := <-stream.responses
require.True(ok)
require.NotNil(resp)
require.NotNil(resp.msg)
require.Nil(resp.err)
}()
wg.Wait()
require.True(stream.serverClosed)
require.True(stream.IsServerClosed())
_, ok = <-stream.responses
require.False(ok)
}
func TestNewPutObjectStream(t *testing.T) {
require := tr.New(t)
stream := newPutObjectStream()
require.NotNil(stream)
require.NotNil(stream.m)
require.NotNil(stream.client)
require.NotNil(stream.server)
require.NotNil(stream.requests)
require.NotNil(stream.responses)
require.False(stream.clientClosed)
require.False(stream.IsClientClosed())
require.False(stream.serverClosed)
require.False(stream.IsServerClosed())
// Validate CloseClient() closes the requests channel
stream.CloseClient()
require.True(stream.clientClosed)
require.True(stream.IsClientClosed())
_, ok := <-stream.requests
require.False(ok)
// Validate CloseServer() closes the responses channel
stream.CloseServer()
require.True(stream.serverClosed)
require.True(stream.IsServerClosed())
_, ok = <-stream.responses
require.False(ok)
}
func Test_GetObjectStream(t *testing.T) {
require := tr.New(t)
@@ -508,95 +272,3 @@ func Test_GetObjectStream(t *testing.T) {
_, ok = <-stream.messages
require.False(ok)
}
func Test_PutObjectStream(t *testing.T) {
require := tr.New(t)
stream := newPutObjectStream()
// Validate the client recieves the response when the server closes the stream
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for {
resp, err := stream.client.CloseAndRecv()
require.NotNil(resp)
require.NoError(err)
break
}
}()
go func() {
defer wg.Done()
err := stream.server.SendAndClose(&plgpb.PutObjectResponse{})
require.NoError(err)
}()
wg.Wait()
require.True(stream.clientClosed)
require.True(stream.IsClientClosed())
require.True(stream.serverClosed)
require.True(stream.IsServerClosed())
_, ok := <-stream.requests
require.False(ok)
_, ok = <-stream.responses
require.False(ok)
// Reset Stream
stream = newPutObjectStream()
// Validate the client recieves an error when the server sends an error
wg.Add(2)
go func() {
defer wg.Done()
for {
resp, err := stream.client.CloseAndRecv()
require.Nil(resp)
require.Error(err)
require.Contains(err.Error(), "mock error")
break
}
}()
go func() {
defer wg.Done()
err := stream.server.SendMsg(fmt.Errorf("mock error"))
require.NoError(err)
}()
wg.Wait()
require.True(stream.clientClosed)
require.True(stream.IsClientClosed())
require.True(stream.serverClosed)
require.True(stream.IsServerClosed())
_, ok = <-stream.requests
require.False(ok)
_, ok = <-stream.responses
require.False(ok)
// Reset Stream
stream = newPutObjectStream()
// Validate the server recieves an EOF error when the client closes the stream
wg.Add(2)
go func() {
defer wg.Done()
for {
req, err := stream.server.Recv()
if err != nil {
require.Nil(req)
require.Equal(io.EOF, err)
break
}
}
}()
go func() {
wg.Done()
err := stream.client.CloseSend()
require.NoError(err)
}()
wg.Wait()
require.True(stream.clientClosed)
require.True(stream.IsClientClosed())
require.False(stream.serverClosed)
require.False(stream.IsServerClosed())
_, ok = <-stream.requests
require.False(ok)
}