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>
261 lines
7.5 KiB
Go
261 lines
7.5 KiB
Go
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)
|
|
}
|
|
}
|