package mockminio import ( "bytes" "context" "crypto/sha256" "encoding/hex" "fmt" "io" "sync" "testing" "gcy_hpc_server/internal/storage" ) func sha256Hex(data []byte) string { h := sha256.Sum256(data) return hex.EncodeToString(h[:]) } func TestNewInMemoryStorage_ReturnsInitialized(t *testing.T) { s := NewInMemoryStorage() if s == nil { t.Fatal("expected non-nil storage") } } func TestPutObject_StoresData(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() data := []byte("hello world") info, err := s.PutObject(ctx, "bucket", "key1", bytes.NewReader(data), int64(len(data)), storage.PutObjectOptions{ContentType: "text/plain"}) if err != nil { t.Fatalf("PutObject: %v", err) } wantETag := sha256Hex(data) if info.ETag != wantETag { t.Errorf("ETag = %q, want %q", info.ETag, wantETag) } if info.Size != int64(len(data)) { t.Errorf("Size = %d, want %d", info.Size, len(data)) } } func TestGetObject_FullObject(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() data := []byte("hello world") s.PutObject(ctx, "bucket", "key1", bytes.NewReader(data), int64(len(data)), storage.PutObjectOptions{}) rc, info, err := s.GetObject(ctx, "bucket", "key1", storage.GetOptions{}) if err != nil { t.Fatalf("GetObject: %v", err) } defer rc.Close() got, err := io.ReadAll(rc) if err != nil { t.Fatalf("ReadAll: %v", err) } if !bytes.Equal(got, data) { t.Errorf("got %q, want %q", got, data) } if info.Size != int64(len(data)) { t.Errorf("info.Size = %d, want %d", info.Size, len(data)) } } func TestGetObject_RangeStartOnly(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() data := []byte("0123456789") s.PutObject(ctx, "bucket", "key1", bytes.NewReader(data), int64(len(data)), storage.PutObjectOptions{}) start := int64(5) rc, _, err := s.GetObject(ctx, "bucket", "key1", storage.GetOptions{Start: &start}) if err != nil { t.Fatalf("GetObject: %v", err) } defer rc.Close() got, err := io.ReadAll(rc) if err != nil { t.Fatalf("ReadAll: %v", err) } want := data[5:] if !bytes.Equal(got, want) { t.Errorf("got %q, want %q", got, want) } } func TestGetObject_RangeEndOnly(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() data := []byte("0123456789") s.PutObject(ctx, "bucket", "key1", bytes.NewReader(data), int64(len(data)), storage.PutObjectOptions{}) end := int64(4) rc, _, err := s.GetObject(ctx, "bucket", "key1", storage.GetOptions{End: &end}) if err != nil { t.Fatalf("GetObject: %v", err) } defer rc.Close() got, err := io.ReadAll(rc) if err != nil { t.Fatalf("ReadAll: %v", err) } want := data[:5] if !bytes.Equal(got, want) { t.Errorf("got %q, want %q", got, want) } } func TestGetObject_RangeStartAndEnd(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() data := []byte("0123456789") s.PutObject(ctx, "bucket", "key1", bytes.NewReader(data), int64(len(data)), storage.PutObjectOptions{}) start := int64(2) end := int64(5) rc, _, err := s.GetObject(ctx, "bucket", "key1", storage.GetOptions{Start: &start, End: &end}) if err != nil { t.Fatalf("GetObject: %v", err) } defer rc.Close() got, err := io.ReadAll(rc) if err != nil { t.Fatalf("ReadAll: %v", err) } want := data[2:6] if !bytes.Equal(got, want) { t.Errorf("got %q, want %q", got, want) } } func TestGetObject_NotFound(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() _, _, err := s.GetObject(ctx, "bucket", "nonexistent", storage.GetOptions{}) if err == nil { t.Fatal("expected error for missing object") } } func TestComposeObject_ConcatenatesSources(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() s.PutObject(ctx, "bucket", "part1", bytes.NewReader([]byte("hello ")), 6, storage.PutObjectOptions{}) s.PutObject(ctx, "bucket", "part2", bytes.NewReader([]byte("world")), 5, storage.PutObjectOptions{}) info, err := s.ComposeObject(ctx, "bucket", "combined", []string{"part1", "part2"}) if err != nil { t.Fatalf("ComposeObject: %v", err) } want := []byte("hello world") if info.Size != int64(len(want)) { t.Errorf("Size = %d, want %d", info.Size, len(want)) } wantETag := sha256Hex(want) if info.ETag != wantETag { t.Errorf("ETag = %q, want %q", info.ETag, wantETag) } rc, _, err := s.GetObject(ctx, "bucket", "combined", storage.GetOptions{}) if err != nil { t.Fatalf("GetObject combined: %v", err) } defer rc.Close() got, _ := io.ReadAll(rc) if !bytes.Equal(got, want) { t.Errorf("got %q, want %q", got, want) } } func TestComposeObject_MissingSource(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() _, err := s.ComposeObject(ctx, "bucket", "dst", []string{"missing"}) if err == nil { t.Fatal("expected error for missing source") } } func TestRemoveObject(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() s.PutObject(ctx, "bucket", "key1", bytes.NewReader([]byte("data")), 4, storage.PutObjectOptions{}) err := s.RemoveObject(ctx, "bucket", "key1", storage.RemoveObjectOptions{}) if err != nil { t.Fatalf("RemoveObject: %v", err) } _, _, err = s.GetObject(ctx, "bucket", "key1", storage.GetOptions{}) if err == nil { t.Fatal("expected error after removal") } } func TestRemoveObjects(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() for i := 0; i < 5; i++ { key := fmt.Sprintf("key%d", i) s.PutObject(ctx, "bucket", key, bytes.NewReader([]byte(key)), int64(len(key)), storage.PutObjectOptions{}) } err := s.RemoveObjects(ctx, "bucket", []string{"key1", "key3"}, storage.RemoveObjectsOptions{}) if err != nil { t.Fatalf("RemoveObjects: %v", err) } objects, _ := s.ListObjects(ctx, "bucket", "", true) if len(objects) != 3 { t.Errorf("got %d objects, want 3", len(objects)) } } func TestListObjects(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() s.PutObject(ctx, "bucket", "dir/a", bytes.NewReader([]byte("a")), 1, storage.PutObjectOptions{}) s.PutObject(ctx, "bucket", "dir/b", bytes.NewReader([]byte("bb")), 2, storage.PutObjectOptions{}) s.PutObject(ctx, "bucket", "other/c", bytes.NewReader([]byte("ccc")), 3, storage.PutObjectOptions{}) objects, err := s.ListObjects(ctx, "bucket", "dir/", true) if err != nil { t.Fatalf("ListObjects: %v", err) } if len(objects) != 2 { t.Fatalf("got %d objects, want 2", len(objects)) } if objects[0].Key != "dir/a" || objects[1].Key != "dir/b" { t.Errorf("keys = %v, want [dir/a dir/b]", []string{objects[0].Key, objects[1].Key}) } if objects[0].Size != 1 || objects[1].Size != 2 { t.Errorf("sizes = %v, want [1 2]", []int64{objects[0].Size, objects[1].Size}) } } func TestBucketExists(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() ok, _ := s.BucketExists(ctx, "mybucket") if ok { t.Error("bucket should not exist yet") } s.MakeBucket(ctx, "mybucket", storage.MakeBucketOptions{}) ok, _ = s.BucketExists(ctx, "mybucket") if !ok { t.Error("bucket should exist after MakeBucket") } } func TestMakeBucket(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() err := s.MakeBucket(ctx, "test-bucket", storage.MakeBucketOptions{Region: "us-east-1"}) if err != nil { t.Fatalf("MakeBucket: %v", err) } ok, _ := s.BucketExists(ctx, "test-bucket") if !ok { t.Error("bucket should exist") } } func TestStatObject(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() data := []byte("test data") s.PutObject(ctx, "bucket", "key1", bytes.NewReader(data), int64(len(data)), storage.PutObjectOptions{ContentType: "text/plain"}) info, err := s.StatObject(ctx, "bucket", "key1", storage.StatObjectOptions{}) if err != nil { t.Fatalf("StatObject: %v", err) } wantETag := sha256Hex(data) if info.Key != "key1" { t.Errorf("Key = %q, want %q", info.Key, "key1") } if info.Size != int64(len(data)) { t.Errorf("Size = %d, want %d", info.Size, len(data)) } if info.ETag != wantETag { t.Errorf("ETag = %q, want %q", info.ETag, wantETag) } if info.ContentType != "text/plain" { t.Errorf("ContentType = %q, want %q", info.ContentType, "text/plain") } if info.LastModified.IsZero() { t.Error("LastModified should not be zero") } } func TestStatObject_NotFound(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() _, err := s.StatObject(ctx, "bucket", "nonexistent", storage.StatObjectOptions{}) if err == nil { t.Fatal("expected error for missing object") } } func TestAbortMultipartUpload(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() err := s.AbortMultipartUpload(ctx, "bucket", "key", "upload-id") if err != nil { t.Fatalf("AbortMultipartUpload: %v", err) } } func TestRemoveIncompleteUpload(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() err := s.RemoveIncompleteUpload(ctx, "bucket", "key") if err != nil { t.Fatalf("RemoveIncompleteUpload: %v", err) } } func TestConcurrentAccess(t *testing.T) { s := NewInMemoryStorage() ctx := context.Background() const goroutines = 50 var wg sync.WaitGroup wg.Add(goroutines * 2) for i := 0; i < goroutines; i++ { go func(i int) { defer wg.Done() key := fmt.Sprintf("key%d", i%10) data := []byte(fmt.Sprintf("data-%d", i)) s.PutObject(ctx, "bucket", key, bytes.NewReader(data), int64(len(data)), storage.PutObjectOptions{}) }(i) go func(i int) { defer wg.Done() key := fmt.Sprintf("key%d", i%10) rc, _, _ := s.GetObject(ctx, "bucket", key, storage.GetOptions{}) if rc != nil { rc.Close() } }(i) } wg.Wait() }