Files
spinifex/hive/objectstore/objectstore_test.go
T
2026-02-10 09:23:25 +10:00

335 lines
8.3 KiB
Go

package objectstore
import (
"bytes"
"io"
"sync"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMemoryObjectStore_PutAndGet(t *testing.T) {
store := NewMemoryObjectStore()
// Put an object
putInput := &s3.PutObjectInput{
Bucket: aws.String("test-bucket"),
Key: aws.String("test-key"),
Body: bytes.NewReader([]byte("test data")),
}
_, err := store.PutObject(putInput)
require.NoError(t, err)
// Get the object
getInput := &s3.GetObjectInput{
Bucket: aws.String("test-bucket"),
Key: aws.String("test-key"),
}
output, err := store.GetObject(getInput)
require.NoError(t, err)
data, _ := io.ReadAll(output.Body)
assert.Equal(t, "test data", string(data))
assert.Equal(t, int64(9), *output.ContentLength)
}
func TestMemoryObjectStore_GetNonExistent(t *testing.T) {
store := NewMemoryObjectStore()
getInput := &s3.GetObjectInput{
Bucket: aws.String("test-bucket"),
Key: aws.String("non-existent"),
}
_, err := store.GetObject(getInput)
assert.Error(t, err)
assert.True(t, IsNoSuchKeyError(err))
// Verify error properties
var noSuchKey *NoSuchKeyError
assert.ErrorAs(t, err, &noSuchKey)
assert.Equal(t, "non-existent", noSuchKey.Key)
assert.Equal(t, "NoSuchKey", noSuchKey.Code())
}
func TestMemoryObjectStore_Delete(t *testing.T) {
store := NewMemoryObjectStore()
// Put an object
putInput := &s3.PutObjectInput{
Bucket: aws.String("test-bucket"),
Key: aws.String("to-delete"),
Body: bytes.NewReader([]byte("data")),
}
_, err := store.PutObject(putInput)
require.NoError(t, err)
// Verify it exists
assert.Equal(t, 1, store.Count())
// Delete the object
deleteInput := &s3.DeleteObjectInput{
Bucket: aws.String("test-bucket"),
Key: aws.String("to-delete"),
}
_, err = store.DeleteObject(deleteInput)
require.NoError(t, err)
// Verify it's gone
assert.Equal(t, 0, store.Count())
// Getting it should fail
_, err = store.GetObject(&s3.GetObjectInput{
Bucket: aws.String("test-bucket"),
Key: aws.String("to-delete"),
})
assert.True(t, IsNoSuchKeyError(err))
}
func TestMemoryObjectStore_DeleteNonExistent(t *testing.T) {
store := NewMemoryObjectStore()
// Deleting non-existent should not error (matches S3 behavior)
deleteInput := &s3.DeleteObjectInput{
Bucket: aws.String("test-bucket"),
Key: aws.String("never-existed"),
}
_, err := store.DeleteObject(deleteInput)
assert.NoError(t, err)
}
func TestMemoryObjectStore_ListObjects(t *testing.T) {
store := NewMemoryObjectStore()
// Put multiple objects
objects := map[string]string{
"images/ami-1.json": `{"id":"ami-1"}`,
"images/ami-2.json": `{"id":"ami-2"}`,
"images/ami-3.json": `{"id":"ami-3"}`,
"volumes/vol-1.json": `{"id":"vol-1"}`,
"snapshots/snap-1.json": `{"id":"snap-1"}`,
}
for key, value := range objects {
_, err := store.PutObject(&s3.PutObjectInput{
Bucket: aws.String("hive-metadata"),
Key: aws.String(key),
Body: bytes.NewReader([]byte(value)),
})
require.NoError(t, err)
}
// List all objects in bucket
output, err := store.ListObjects(&s3.ListObjectsInput{
Bucket: aws.String("hive-metadata"),
})
require.NoError(t, err)
assert.Len(t, output.Contents, 5)
// List objects with prefix
output, err = store.ListObjects(&s3.ListObjectsInput{
Bucket: aws.String("hive-metadata"),
Prefix: aws.String("images/"),
})
require.NoError(t, err)
assert.Len(t, output.Contents, 3)
// Verify keys
keys := make([]string, len(output.Contents))
for i, obj := range output.Contents {
keys[i] = *obj.Key
}
assert.Contains(t, keys, "images/ami-1.json")
assert.Contains(t, keys, "images/ami-2.json")
assert.Contains(t, keys, "images/ami-3.json")
}
func TestMemoryObjectStore_ListObjectsWithDelimiter(t *testing.T) {
store := NewMemoryObjectStore()
// Put objects with directory-like structure
objects := []string{
"root.json",
"images/ami-1.json",
"images/ami-2.json",
"volumes/vol-1.json",
"volumes/snapshots/snap-1.json",
}
for _, key := range objects {
_, err := store.PutObject(&s3.PutObjectInput{
Bucket: aws.String("hive"),
Key: aws.String(key),
Body: bytes.NewReader([]byte("{}")),
})
require.NoError(t, err)
}
// List with delimiter at root level
output, err := store.ListObjects(&s3.ListObjectsInput{
Bucket: aws.String("hive"),
Delimiter: aws.String("/"),
})
require.NoError(t, err)
// Should have one file at root
assert.Len(t, output.Contents, 1)
assert.Equal(t, "root.json", *output.Contents[0].Key)
// Should have two common prefixes (images/, volumes/)
assert.Len(t, output.CommonPrefixes, 2)
}
func TestMemoryObjectStore_Clear(t *testing.T) {
store := NewMemoryObjectStore()
// Add some objects
for i := range 5 {
_, _ = store.PutObject(&s3.PutObjectInput{
Bucket: aws.String("bucket"),
Key: aws.String("key-" + string(rune('0'+i))),
Body: bytes.NewReader([]byte("data")),
})
}
assert.Equal(t, 5, store.Count())
// Clear
store.Clear()
assert.Equal(t, 0, store.Count())
}
func TestMemoryObjectStore_MultipleBuckets(t *testing.T) {
store := NewMemoryObjectStore()
// Put objects in different buckets
_, _ = store.PutObject(&s3.PutObjectInput{
Bucket: aws.String("bucket-a"),
Key: aws.String("key1"),
Body: bytes.NewReader([]byte("data-a")),
})
_, _ = store.PutObject(&s3.PutObjectInput{
Bucket: aws.String("bucket-b"),
Key: aws.String("key1"),
Body: bytes.NewReader([]byte("data-b")),
})
// List bucket-a
outputA, _ := store.ListObjects(&s3.ListObjectsInput{
Bucket: aws.String("bucket-a"),
})
assert.Len(t, outputA.Contents, 1)
// List bucket-b
outputB, _ := store.ListObjects(&s3.ListObjectsInput{
Bucket: aws.String("bucket-b"),
})
assert.Len(t, outputB.Contents, 1)
// Get from bucket-a
objA, _ := store.GetObject(&s3.GetObjectInput{
Bucket: aws.String("bucket-a"),
Key: aws.String("key1"),
})
dataA, _ := io.ReadAll(objA.Body)
assert.Equal(t, "data-a", string(dataA))
// Get from bucket-b
objB, _ := store.GetObject(&s3.GetObjectInput{
Bucket: aws.String("bucket-b"),
Key: aws.String("key1"),
})
dataB, _ := io.ReadAll(objB.Body)
assert.Equal(t, "data-b", string(dataB))
}
func TestMemoryObjectStore_Overwrite(t *testing.T) {
store := NewMemoryObjectStore()
// Put initial object
_, _ = store.PutObject(&s3.PutObjectInput{
Bucket: aws.String("bucket"),
Key: aws.String("key"),
Body: bytes.NewReader([]byte("initial")),
})
// Overwrite
_, _ = store.PutObject(&s3.PutObjectInput{
Bucket: aws.String("bucket"),
Key: aws.String("key"),
Body: bytes.NewReader([]byte("updated")),
})
// Verify overwrite
output, _ := store.GetObject(&s3.GetObjectInput{
Bucket: aws.String("bucket"),
Key: aws.String("key"),
})
data, _ := io.ReadAll(output.Body)
assert.Equal(t, "updated", string(data))
// Should still be one object
assert.Equal(t, 1, store.Count())
}
func TestMemoryObjectStore_ConcurrentAccess(t *testing.T) {
store := NewMemoryObjectStore()
var wg sync.WaitGroup
numGoroutines := 100
// Concurrent writes
wg.Add(numGoroutines)
for i := range numGoroutines {
go func(idx int) {
defer wg.Done()
_, _ = store.PutObject(&s3.PutObjectInput{
Bucket: aws.String("bucket"),
Key: aws.String("key-" + string(rune('a'+idx%26)) + "-" + string(rune('0'+idx/26))),
Body: bytes.NewReader([]byte("data")),
})
}(i)
}
wg.Wait()
// Concurrent reads
wg.Add(numGoroutines)
for range numGoroutines {
go func() {
defer wg.Done()
_, _ = store.ListObjects(&s3.ListObjectsInput{
Bucket: aws.String("bucket"),
})
}()
}
wg.Wait()
// Should complete without race conditions
assert.True(t, store.Count() > 0)
}
func TestNoSuchKeyError(t *testing.T) {
err := &NoSuchKeyError{Key: "test-key"}
assert.Equal(t, "NoSuchKey: test-key", err.Error())
assert.Equal(t, "NoSuchKey", err.Code())
assert.True(t, IsNoSuchKeyError(err))
assert.False(t, IsNoSuchKeyError(nil))
assert.False(t, IsNoSuchKeyError(assert.AnError))
}
func TestIsNoSuchKeyError_WithWrappedError(t *testing.T) {
originalErr := &NoSuchKeyError{Key: "test"}
// Even when wrapped, IsNoSuchKeyError should work
assert.True(t, IsNoSuchKeyError(originalErr))
}
// Test that the interface is properly defined
var _ ObjectStore = (*MemoryObjectStore)(nil)
var _ ObjectStore = (*S3ObjectStore)(nil)