feat(service): add upload, download, file, and folder services
Add UploadService (dedup, chunk lifecycle, ComposeObject), DownloadService (Range support), FileService (ref counting), FolderService (path validation). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
260
internal/service/download_service_test.go
Normal file
260
internal/service/download_service_test.go
Normal file
@@ -0,0 +1,260 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user