package service import ( "bytes" "context" "io" "testing" "time" "gcy_hpc_server/internal/model" "gcy_hpc_server/internal/storage" "gcy_hpc_server/internal/store" "go.uber.org/zap" gormlogger "gorm.io/gorm/logger" "gorm.io/driver/sqlite" "gorm.io/gorm" ) type mockDownloadStorage struct { getObjectFn func(ctx context.Context, bucket, key string, opts storage.GetOptions) (io.ReadCloser, storage.ObjectInfo, error) } func (m *mockDownloadStorage) PutObject(_ context.Context, _ string, _ string, _ io.Reader, _ int64, _ storage.PutObjectOptions) (storage.UploadInfo, error) { return storage.UploadInfo{}, nil } func (m *mockDownloadStorage) GetObject(ctx context.Context, bucket, key string, opts storage.GetOptions) (io.ReadCloser, storage.ObjectInfo, error) { if m.getObjectFn != nil { return m.getObjectFn(ctx, bucket, key, opts) } return nil, storage.ObjectInfo{}, nil } func (m *mockDownloadStorage) ComposeObject(_ context.Context, _ string, _ string, _ []string) (storage.UploadInfo, error) { return storage.UploadInfo{}, nil } func (m *mockDownloadStorage) AbortMultipartUpload(_ context.Context, _ string, _ string, _ string) error { return nil } func (m *mockDownloadStorage) RemoveIncompleteUpload(_ context.Context, _ string, _ string) error { return nil } func (m *mockDownloadStorage) RemoveObject(_ context.Context, _ string, _ string, _ storage.RemoveObjectOptions) error { return nil } func (m *mockDownloadStorage) ListObjects(_ context.Context, _ string, _ string, _ bool) ([]storage.ObjectInfo, error) { return nil, nil } func (m *mockDownloadStorage) RemoveObjects(_ context.Context, _ string, _ []string, _ storage.RemoveObjectsOptions) error { return nil } func (m *mockDownloadStorage) BucketExists(_ context.Context, _ string) (bool, error) { return true, nil } func (m *mockDownloadStorage) MakeBucket(_ context.Context, _ string, _ storage.MakeBucketOptions) error { return nil } func (m *mockDownloadStorage) StatObject(_ context.Context, _ string, _ string, _ storage.StatObjectOptions) (storage.ObjectInfo, error) { return storage.ObjectInfo{}, nil } func setupDownloadService(t *testing.T) (*DownloadService, *store.FileStore, *store.BlobStore, *mockDownloadStorage) { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: gormlogger.Default.LogMode(gormlogger.Silent), }) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate(&model.File{}, &model.FileBlob{}); err != nil { t.Fatalf("auto migrate: %v", err) } mockStorage := &mockDownloadStorage{} blobStore := store.NewBlobStore(db) fileStore := store.NewFileStore(db) svc := NewDownloadService(mockStorage, blobStore, fileStore, "test-bucket", zap.NewNop()) return svc, fileStore, blobStore, mockStorage } func createTestFileAndBlob(t *testing.T, fileStore *store.FileStore, blobStore *store.BlobStore) (*model.File, *model.FileBlob) { t.Helper() blob := &model.FileBlob{ SHA256: "abc123def456abc123def456abc123def456abc123def456abc123def456abcd", MinioKey: "chunks/session1/part-0", FileSize: 5000, MimeType: "application/octet-stream", RefCount: 1, } if err := blobStore.Create(context.Background(), blob); err != nil { t.Fatalf("create blob: %v", err) } file := &model.File{ Name: "test.dat", BlobSHA256: blob.SHA256, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := fileStore.Create(context.Background(), file); err != nil { t.Fatalf("create file: %v", err) } return file, blob } func TestDownload_FullFile(t *testing.T) { svc, fileStore, blobStore, mockStorage := setupDownloadService(t) file, blob := createTestFileAndBlob(t, fileStore, blobStore) content := make([]byte, blob.FileSize) for i := range content { content[i] = byte(i % 256) } mockStorage.getObjectFn = func(_ context.Context, _, _ string, opts storage.GetOptions) (io.ReadCloser, storage.ObjectInfo, error) { if opts.Start == nil || opts.End == nil { t.Fatal("expected Start and End to be set") } if *opts.Start != 0 { t.Fatalf("expected start=0, got %d", *opts.Start) } if *opts.End != blob.FileSize-1 { t.Fatalf("expected end=%d, got %d", blob.FileSize-1, *opts.End) } return io.NopCloser(bytes.NewReader(content)), storage.ObjectInfo{Size: blob.FileSize}, nil } reader, gotFile, gotBlob, start, end, err := svc.Download(context.Background(), file.ID, "") if err != nil { t.Fatalf("Download: %v", err) } defer reader.Close() if gotFile.ID != file.ID { t.Fatalf("expected file ID %d, got %d", file.ID, gotFile.ID) } if gotBlob.SHA256 != blob.SHA256 { t.Fatalf("expected blob SHA256 %s, got %s", blob.SHA256, gotBlob.SHA256) } if start != 0 { t.Fatalf("expected start=0, got %d", start) } if end != blob.FileSize-1 { t.Fatalf("expected end=%d, got %d", blob.FileSize-1, end) } read, _ := io.ReadAll(reader) if int64(len(read)) != blob.FileSize { t.Fatalf("expected %d bytes, got %d", blob.FileSize, len(read)) } } func TestDownload_WithRange(t *testing.T) { svc, fileStore, blobStore, mockStorage := setupDownloadService(t) file, _ := createTestFileAndBlob(t, fileStore, blobStore) content := make([]byte, 1024) for i := range content { content[i] = byte(i % 256) } mockStorage.getObjectFn = func(_ context.Context, _, _ string, opts storage.GetOptions) (io.ReadCloser, storage.ObjectInfo, error) { if opts.Start == nil || opts.End == nil { t.Fatal("expected Start and End to be set") } if *opts.Start != 0 { t.Fatalf("expected start=0, got %d", *opts.Start) } if *opts.End != 1023 { t.Fatalf("expected end=1023, got %d", *opts.End) } return io.NopCloser(bytes.NewReader(content[:1024])), storage.ObjectInfo{Size: 1024}, nil } reader, _, _, start, end, err := svc.Download(context.Background(), file.ID, "bytes=0-1023") if err != nil { t.Fatalf("Download: %v", err) } defer reader.Close() if start != 0 { t.Fatalf("expected start=0, got %d", start) } if end != 1023 { t.Fatalf("expected end=1023, got %d", end) } read, _ := io.ReadAll(reader) if len(read) != 1024 { t.Fatalf("expected 1024 bytes, got %d", len(read)) } } func TestDownload_FileNotFound(t *testing.T) { svc, _, _, _ := setupDownloadService(t) _, _, _, _, _, err := svc.Download(context.Background(), 99999, "") if err == nil { t.Fatal("expected error for missing file") } if err.Error() != "file not found" { t.Fatalf("unexpected error: %v", err) } } func TestDownload_BlobNotFound(t *testing.T) { svc, fileStore, _, _ := setupDownloadService(t) file := &model.File{ Name: "orphan.dat", BlobSHA256: "nonexistent_hash_0000000000000000000000000000000000000000", CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := fileStore.Create(context.Background(), file); err != nil { t.Fatalf("create file: %v", err) } _, _, _, _, _, err := svc.Download(context.Background(), file.ID, "") if err == nil { t.Fatal("expected error for missing blob") } if err.Error() != "blob not found" { t.Fatalf("unexpected error: %v", err) } } func TestGetFileMetadata(t *testing.T) { svc, fileStore, blobStore, _ := setupDownloadService(t) file, _ := createTestFileAndBlob(t, fileStore, blobStore) gotFile, gotBlob, err := svc.GetFileMetadata(context.Background(), file.ID) if err != nil { t.Fatalf("GetFileMetadata: %v", err) } if gotFile.ID != file.ID { t.Fatalf("expected file ID %d, got %d", file.ID, gotFile.ID) } if gotBlob.FileSize != 5000 { t.Fatalf("expected file size 5000, got %d", gotBlob.FileSize) } if gotBlob.MimeType != "application/octet-stream" { t.Fatalf("expected mime type application/octet-stream, got %s", gotBlob.MimeType) } }