feat: storage plugin list objects (#6723)
* feat(proto): add storage plugin proto definitions for session recording recovery Co-authored-by: Damian Debkowski <damian.debkowski@hashicorp.com> Co-authored-by: Ryan Derr <ryan.derr@hashicorp.com> * feat(gen): add generated storage plugin code for session recording recovery Co-authored-by: Damian Debkowski <damian.debkowski@hashicorp.com> Co-authored-by: Manjeet Singh <manjeet.singh@hashicorp.com> * feat(plugin): add plugin loopback support for session recording recovery Co-authored-by: Ryan Derr <ryan.derr@hashicorp.com> Co-authored-by: aditya2548 <aditya25dec2000@gmail.com> --------- Co-authored-by: Ryan Derr <ryan.derr@hashicorp.com> Co-authored-by: Manjeet Singh <manjeet.singh@hashicorp.com> Co-authored-by: aditya2548 <aditya25dec2000@gmail.com>
This commit is contained in:
9 files changed
+1771
-103
No files matched your search
@@ -0,0 +1,549 @@
|
||||
// Copyright IBM Corp. 2024, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package loopback
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||
plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
func TestPluginMockError_match(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testBucket := &storagebuckets.StorageBucket{
|
||||
BucketName: "test-bucket",
|
||||
BucketPrefix: "test-prefix",
|
||||
}
|
||||
|
||||
testBucketNoPrefix := &storagebuckets.StorageBucket{
|
||||
BucketName: "test-bucket",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mockError PluginMockError
|
||||
bucket *storagebuckets.StorageBucket
|
||||
key string
|
||||
method Method
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "exact match with all fields",
|
||||
mockError: PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
BucketPrefix: "test-prefix",
|
||||
ObjectKey: "test-key",
|
||||
ErrMethod: PutObject,
|
||||
},
|
||||
bucket: testBucket,
|
||||
key: "test-key",
|
||||
method: PutObject,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "match with Any method",
|
||||
mockError: PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
BucketPrefix: "test-prefix",
|
||||
ObjectKey: "test-key",
|
||||
ErrMethod: Any,
|
||||
},
|
||||
bucket: testBucket,
|
||||
key: "test-key",
|
||||
method: GetObject,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "match with empty key (create/update/delete operations)",
|
||||
mockError: PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
BucketPrefix: "test-prefix",
|
||||
ErrMethod: OnCreateStorageBucket,
|
||||
},
|
||||
bucket: testBucket,
|
||||
key: "", // Empty key for bucket operations
|
||||
method: OnCreateStorageBucket,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "match with path prefix",
|
||||
mockError: PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
BucketPrefix: "test-prefix",
|
||||
ObjectKey: "logs/session123/data.log", // Must match the key exactly
|
||||
ErrPath: "logs/", // Path prefix also matches
|
||||
ErrMethod: PutObject,
|
||||
},
|
||||
bucket: testBucket,
|
||||
key: "logs/session123/data.log",
|
||||
method: PutObject,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no match - different bucket name",
|
||||
mockError: PluginMockError{
|
||||
BucketName: "different-bucket",
|
||||
ErrMethod: PutObject,
|
||||
},
|
||||
bucket: testBucket,
|
||||
key: "test-key",
|
||||
method: PutObject,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no match - different object key",
|
||||
mockError: PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
ObjectKey: "different-key",
|
||||
ErrMethod: PutObject,
|
||||
},
|
||||
bucket: testBucket,
|
||||
key: "test-key",
|
||||
method: PutObject,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no match - different method",
|
||||
mockError: PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
ObjectKey: "test-key",
|
||||
ErrMethod: GetObject,
|
||||
},
|
||||
bucket: testBucket,
|
||||
key: "test-key",
|
||||
method: PutObject,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no match - different bucket prefix",
|
||||
mockError: PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
BucketPrefix: "different-prefix",
|
||||
ErrMethod: PutObject,
|
||||
},
|
||||
bucket: testBucket,
|
||||
key: "test-key",
|
||||
method: PutObject,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no match - path prefix doesn't match",
|
||||
mockError: PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
ObjectKey: "", // ObjectKey should be empty when using ErrPath
|
||||
ErrPath: "logs/",
|
||||
ErrMethod: PutObject,
|
||||
},
|
||||
bucket: testBucketNoPrefix,
|
||||
key: "data/session123/info.json",
|
||||
method: PutObject,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "match - bucket with no prefix requirement",
|
||||
mockError: PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
ObjectKey: "test-key",
|
||||
ErrMethod: PutObject,
|
||||
},
|
||||
bucket: testBucketNoPrefix,
|
||||
key: "test-key",
|
||||
method: PutObject,
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.mockError.match(tt.bucket, tt.key, tt.method)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginMockPutObjectResponse_match(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testBucket := &storagebuckets.StorageBucket{
|
||||
BucketName: "test-bucket",
|
||||
BucketPrefix: "test-prefix",
|
||||
}
|
||||
|
||||
testBucketNoPrefix := &storagebuckets.StorageBucket{
|
||||
BucketName: "test-bucket",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mockResponse PluginMockPutObjectResponse
|
||||
bucket *storagebuckets.StorageBucket
|
||||
key string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "exact match with all fields",
|
||||
mockResponse: PluginMockPutObjectResponse{
|
||||
BucketName: "test-bucket",
|
||||
BucketPrefix: "test-prefix",
|
||||
ObjectKey: "test-key",
|
||||
},
|
||||
bucket: testBucket,
|
||||
key: "test-key",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no match - different bucket name",
|
||||
mockResponse: PluginMockPutObjectResponse{
|
||||
BucketName: "different-bucket",
|
||||
ObjectKey: "test-key",
|
||||
},
|
||||
bucket: testBucket,
|
||||
key: "test-key",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no match - different object key",
|
||||
mockResponse: PluginMockPutObjectResponse{
|
||||
BucketName: "test-bucket",
|
||||
ObjectKey: "different-key",
|
||||
},
|
||||
bucket: testBucket,
|
||||
key: "test-key",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no match - different bucket prefix",
|
||||
mockResponse: PluginMockPutObjectResponse{
|
||||
BucketName: "test-bucket",
|
||||
BucketPrefix: "different-prefix",
|
||||
ObjectKey: "test-key",
|
||||
},
|
||||
bucket: testBucket,
|
||||
key: "test-key",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "match - bucket with no prefix requirement",
|
||||
mockResponse: PluginMockPutObjectResponse{
|
||||
BucketName: "test-bucket",
|
||||
ObjectKey: "test-key",
|
||||
},
|
||||
bucket: testBucketNoPrefix,
|
||||
key: "test-key",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.mockResponse.match(tt.bucket, tt.key)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTestOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("default options", func(t *testing.T) {
|
||||
opts, err := getTestOpts()
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := TestOptions{
|
||||
withChunkSize: 8,
|
||||
}
|
||||
assert.Equal(t, expected.withChunkSize, opts.withChunkSize)
|
||||
assert.Nil(t, opts.withMockBuckets)
|
||||
assert.Empty(t, opts.withMockError)
|
||||
assert.Empty(t, opts.withMockPutObjectResponse)
|
||||
})
|
||||
|
||||
t.Run("with mock buckets", func(t *testing.T) {
|
||||
mockBuckets := map[BucketName]Bucket{
|
||||
"test-bucket": make(Bucket),
|
||||
}
|
||||
|
||||
opts, err := getTestOpts(WithMockBuckets(mockBuckets))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, mockBuckets, opts.withMockBuckets)
|
||||
})
|
||||
|
||||
t.Run("with mock error", func(t *testing.T) {
|
||||
mockError := PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
ErrMsg: "test error",
|
||||
ErrCode: codes.Internal,
|
||||
ErrMethod: PutObject,
|
||||
}
|
||||
|
||||
opts, err := getTestOpts(WithMockError(mockError))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, opts.withMockError, 1)
|
||||
assert.Equal(t, mockError, opts.withMockError[0])
|
||||
})
|
||||
|
||||
t.Run("with multiple mock errors", func(t *testing.T) {
|
||||
mockError1 := PluginMockError{
|
||||
BucketName: "test-bucket-1",
|
||||
ErrMsg: "test error 1",
|
||||
ErrCode: codes.Internal,
|
||||
ErrMethod: PutObject,
|
||||
}
|
||||
mockError2 := PluginMockError{
|
||||
BucketName: "test-bucket-2",
|
||||
ErrMsg: "test error 2",
|
||||
ErrCode: codes.NotFound,
|
||||
ErrMethod: GetObject,
|
||||
}
|
||||
|
||||
opts, err := getTestOpts(
|
||||
WithMockError(mockError1),
|
||||
WithMockError(mockError2),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, opts.withMockError, 2)
|
||||
assert.Equal(t, mockError1, opts.withMockError[0])
|
||||
assert.Equal(t, mockError2, opts.withMockError[1])
|
||||
})
|
||||
|
||||
t.Run("with mock put object response", func(t *testing.T) {
|
||||
mockResponse := PluginMockPutObjectResponse{
|
||||
BucketName: "test-bucket",
|
||||
ObjectKey: "test-key",
|
||||
Response: &plgpb.PutObjectResponse{
|
||||
ChecksumSha_256: []byte("test-checksum"),
|
||||
},
|
||||
}
|
||||
|
||||
opts, err := getTestOpts(WithMockPutObjectResponse(mockResponse))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, opts.withMockPutObjectResponse, 1)
|
||||
assert.Equal(t, mockResponse, opts.withMockPutObjectResponse[0])
|
||||
})
|
||||
|
||||
t.Run("with custom chunk size", func(t *testing.T) {
|
||||
customChunkSize := 16
|
||||
|
||||
opts, err := getTestOpts(WithChunkSize(customChunkSize))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, customChunkSize, opts.withChunkSize)
|
||||
})
|
||||
|
||||
t.Run("with all options combined", func(t *testing.T) {
|
||||
mockBuckets := map[BucketName]Bucket{
|
||||
"test-bucket": make(Bucket),
|
||||
}
|
||||
mockError := PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
ErrMsg: "test error",
|
||||
ErrCode: codes.Internal,
|
||||
ErrMethod: PutObject,
|
||||
}
|
||||
mockResponse := PluginMockPutObjectResponse{
|
||||
BucketName: "test-bucket",
|
||||
ObjectKey: "test-key",
|
||||
Response: &plgpb.PutObjectResponse{
|
||||
ChecksumSha_256: []byte("test-checksum"),
|
||||
},
|
||||
}
|
||||
customChunkSize := 32
|
||||
|
||||
opts, err := getTestOpts(
|
||||
WithMockBuckets(mockBuckets),
|
||||
WithMockError(mockError),
|
||||
WithMockPutObjectResponse(mockResponse),
|
||||
WithChunkSize(customChunkSize),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, mockBuckets, opts.withMockBuckets)
|
||||
require.Len(t, opts.withMockError, 1)
|
||||
assert.Equal(t, mockError, opts.withMockError[0])
|
||||
require.Len(t, opts.withMockPutObjectResponse, 1)
|
||||
assert.Equal(t, mockResponse, opts.withMockPutObjectResponse[0])
|
||||
assert.Equal(t, customChunkSize, opts.withChunkSize)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetDefaultTestOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
opts := getDefaultTestOptions()
|
||||
|
||||
assert.Equal(t, 8, opts.withChunkSize)
|
||||
assert.Nil(t, opts.withMockBuckets)
|
||||
assert.Empty(t, opts.withMockError)
|
||||
assert.Empty(t, opts.withMockPutObjectResponse)
|
||||
}
|
||||
|
||||
func TestMethod_Constants(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Test that Method constants have expected values
|
||||
assert.Equal(t, Method(0), Any)
|
||||
assert.Equal(t, Method(1), OnCreateStorageBucket)
|
||||
assert.Equal(t, Method(2), OnUpdateStorageBucket)
|
||||
assert.Equal(t, Method(3), OnDeleteStorageBucket)
|
||||
assert.Equal(t, Method(4), ValidatePermissions)
|
||||
assert.Equal(t, Method(5), HeadObject)
|
||||
assert.Equal(t, Method(6), GetObject)
|
||||
assert.Equal(t, Method(7), PutObject)
|
||||
assert.Equal(t, Method(8), DeleteObjects)
|
||||
}
|
||||
|
||||
func TestPluginMockError_ComplexScenarios(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("storage bucket credential state scenarios", func(t *testing.T) {
|
||||
bucket := &storagebuckets.StorageBucket{
|
||||
BucketName: "test-bucket",
|
||||
}
|
||||
|
||||
credentialState := &plgpb.StorageBucketCredentialState{
|
||||
State: &plgpb.Permissions{
|
||||
Write: &plgpb.Permission{
|
||||
State: plgpb.StateType_STATE_TYPE_ERROR,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockError := PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
ErrMethod: ValidatePermissions,
|
||||
StorageBucketCredentialState: credentialState,
|
||||
}
|
||||
|
||||
result := mockError.match(bucket, "", ValidatePermissions)
|
||||
assert.True(t, result)
|
||||
})
|
||||
|
||||
t.Run("path-based error matching", func(t *testing.T) {
|
||||
bucket := &storagebuckets.StorageBucket{
|
||||
BucketName: "test-bucket",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
errPath string
|
||||
objectKey string
|
||||
key string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "exact path match",
|
||||
errPath: "sessions/",
|
||||
objectKey: "sessions/sr_123/data.log",
|
||||
key: "sessions/sr_123/data.log",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "nested path match",
|
||||
errPath: "sessions/sr_123/",
|
||||
objectKey: "sessions/sr_123/connections/cr_456/data.log",
|
||||
key: "sessions/sr_123/connections/cr_456/data.log",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no path match",
|
||||
errPath: "sessions/",
|
||||
objectKey: "recordings/sr_123/data.log",
|
||||
key: "recordings/sr_123/data.log",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "path match but different object key",
|
||||
errPath: "sessions/",
|
||||
objectKey: "sessions/file1.log",
|
||||
key: "sessions/file2.log",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockError := PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
ObjectKey: tt.objectKey,
|
||||
ErrPath: tt.errPath,
|
||||
ErrMethod: PutObject,
|
||||
}
|
||||
|
||||
result := mockError.match(bucket, tt.key, PutObject)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("path-based matching with empty key (bucket operations)", func(t *testing.T) {
|
||||
bucket := &storagebuckets.StorageBucket{
|
||||
BucketName: "test-bucket",
|
||||
}
|
||||
|
||||
t.Run("empty path matches bucket operations", func(t *testing.T) {
|
||||
mockError := PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
ObjectKey: "",
|
||||
ErrPath: "", // Empty path works with empty key
|
||||
ErrMethod: OnCreateStorageBucket,
|
||||
}
|
||||
|
||||
// Empty key for bucket operations (create/update/delete)
|
||||
result := mockError.match(bucket, "", OnCreateStorageBucket)
|
||||
assert.True(t, result)
|
||||
})
|
||||
|
||||
t.Run("non-empty path does not match empty key", func(t *testing.T) {
|
||||
mockError := PluginMockError{
|
||||
BucketName: "test-bucket",
|
||||
ObjectKey: "",
|
||||
ErrPath: "some/path/", // Non-empty path won't match empty key
|
||||
ErrMethod: OnCreateStorageBucket,
|
||||
}
|
||||
|
||||
// Empty key for bucket operations - should not match non-empty path
|
||||
result := mockError.match(bucket, "", OnCreateStorageBucket)
|
||||
assert.False(t, result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestTestOptions_EdgeCases(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("zero chunk size", func(t *testing.T) {
|
||||
opts, err := getTestOpts(WithChunkSize(0))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, opts.withChunkSize)
|
||||
})
|
||||
|
||||
t.Run("negative chunk size", func(t *testing.T) {
|
||||
opts, err := getTestOpts(WithChunkSize(-1))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, -1, opts.withChunkSize)
|
||||
})
|
||||
|
||||
t.Run("empty mock buckets map", func(t *testing.T) {
|
||||
emptyBuckets := make(map[BucketName]Bucket)
|
||||
opts, err := getTestOpts(WithMockBuckets(emptyBuckets))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, opts.withMockBuckets)
|
||||
assert.Empty(t, opts.withMockBuckets)
|
||||
})
|
||||
|
||||
t.Run("nil mock buckets map", func(t *testing.T) {
|
||||
opts, err := getTestOpts(WithMockBuckets(nil))
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, opts.withMockBuckets)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user