mirror of https://github.com/hashicorp/boundary
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
550 lines
14 KiB
550 lines
14 KiB
// 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)
|
|
})
|
|
}
|