s3api_test.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. package s3api
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "sync"
  9. "testing"
  10. "time"
  11. "github.com/TecharoHQ/anubis/lib/store/storetest"
  12. "github.com/aws/aws-sdk-go-v2/aws"
  13. "github.com/aws/aws-sdk-go-v2/service/s3"
  14. )
  15. // mockS3 is an in-memory mock of the methods we use.
  16. type mockS3 struct {
  17. data map[string][]byte
  18. meta map[string]map[string]string
  19. bucket string
  20. mu sync.RWMutex
  21. }
  22. func (m *mockS3) PutObject(ctx context.Context, in *s3.PutObjectInput, _ ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
  23. m.mu.Lock()
  24. defer m.mu.Unlock()
  25. if m.data == nil {
  26. m.data = map[string][]byte{}
  27. }
  28. if m.meta == nil {
  29. m.meta = map[string]map[string]string{}
  30. }
  31. b, _ := io.ReadAll(in.Body)
  32. m.data[aws.ToString(in.Key)] = bytes.Clone(b)
  33. if in.Metadata != nil {
  34. m.meta[aws.ToString(in.Key)] = map[string]string{}
  35. for k, v := range in.Metadata {
  36. m.meta[aws.ToString(in.Key)][k] = v
  37. }
  38. }
  39. m.bucket = aws.ToString(in.Bucket)
  40. return &s3.PutObjectOutput{}, nil
  41. }
  42. func (m *mockS3) GetObject(ctx context.Context, in *s3.GetObjectInput, _ ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
  43. m.mu.RLock()
  44. defer m.mu.RUnlock()
  45. b, ok := m.data[aws.ToString(in.Key)]
  46. if !ok {
  47. return nil, fmt.Errorf("not found")
  48. }
  49. out := &s3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader(b))}
  50. if md, ok := m.meta[aws.ToString(in.Key)]; ok {
  51. out.Metadata = md
  52. }
  53. return out, nil
  54. }
  55. func (m *mockS3) DeleteObject(ctx context.Context, in *s3.DeleteObjectInput, _ ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) {
  56. m.mu.Lock()
  57. defer m.mu.Unlock()
  58. delete(m.data, aws.ToString(in.Key))
  59. delete(m.meta, aws.ToString(in.Key))
  60. return &s3.DeleteObjectOutput{}, nil
  61. }
  62. func (m *mockS3) HeadObject(ctx context.Context, in *s3.HeadObjectInput, _ ...func(*s3.Options)) (*s3.HeadObjectOutput, error) {
  63. m.mu.RLock()
  64. defer m.mu.RUnlock()
  65. if _, ok := m.data[aws.ToString(in.Key)]; !ok {
  66. return nil, fmt.Errorf("not found")
  67. }
  68. return &s3.HeadObjectOutput{}, nil
  69. }
  70. func TestImpl(t *testing.T) {
  71. mock := &mockS3{}
  72. f := Factory{Client: mock}
  73. data, _ := json.Marshal(Config{
  74. BucketName: "bucket",
  75. })
  76. storetest.Common(t, f, json.RawMessage(data))
  77. }
  78. func TestKeyNormalization(t *testing.T) {
  79. mock := &mockS3{}
  80. f := Factory{Client: mock}
  81. data, _ := json.Marshal(Config{
  82. BucketName: "anubis",
  83. })
  84. s, err := f.Build(t.Context(), json.RawMessage(data))
  85. if err != nil {
  86. t.Fatal(err)
  87. }
  88. key := "a:b:c"
  89. val := []byte("value")
  90. if err := s.Set(t.Context(), key, val, 0); err != nil {
  91. t.Fatalf("Set failed: %v", err)
  92. }
  93. // Ensure mock saw normalized key
  94. mock.mu.RLock()
  95. _, hasRaw := mock.data["a:b:c"]
  96. got, hasNorm := mock.data["a/b/c"]
  97. mock.mu.RUnlock()
  98. if hasRaw {
  99. t.Fatalf("mock contains raw key with colon; normalization failed")
  100. }
  101. if !hasNorm || !bytes.Equal(got, val) {
  102. t.Fatalf("normalized key missing or wrong value: got=%q", string(got))
  103. }
  104. // Get using colon key should work
  105. out, err := s.Get(t.Context(), key)
  106. if err != nil {
  107. t.Fatalf("Get failed: %v", err)
  108. }
  109. if !bytes.Equal(out, val) {
  110. t.Fatalf("Get returned wrong value: got=%q", string(out))
  111. }
  112. // Delete using colon key should delete normalized object
  113. if err := s.Delete(t.Context(), key); err != nil {
  114. t.Fatalf("Delete failed: %v", err)
  115. }
  116. // Give any async cleanup in tests a tick (not needed for mock, but harmless)
  117. time.Sleep(1 * time.Millisecond)
  118. mock.mu.RLock()
  119. _, exists := mock.data["a/b/c"]
  120. mock.mu.RUnlock()
  121. if exists {
  122. t.Fatalf("normalized key still exists after Delete")
  123. }
  124. }