package service import ( "bytes" "context" "io" "strings" "sync" "testing" "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/dhao2001/mygo/internal/model" "github.com/dhao2001/mygo/internal/repository" "github.com/dhao2001/mygo/internal/storage" ) // memStorage is an in-memory implementation of storage.StorageBackend for tests. type memStorage struct { mu sync.RWMutex files map[string][]byte } func newMemStorage() *memStorage { return &memStorage{files: make(map[string][]byte)} } func (s *memStorage) Save(_ context.Context, path string, reader io.Reader) (int64, error) { data, err := io.ReadAll(reader) if err != nil { return 0, err } s.mu.Lock() s.files[path] = data s.mu.Unlock() return int64(len(data)), nil } func (s *memStorage) Open(_ context.Context, path string) (io.ReadCloser, error) { s.mu.RLock() data, ok := s.files[path] s.mu.RUnlock() if !ok { return nil, io.ErrUnexpectedEOF } return io.NopCloser(bytes.NewReader(data)), nil } func (s *memStorage) Delete(_ context.Context, path string) error { s.mu.Lock() delete(s.files, path) s.mu.Unlock() return nil } var _ storage.StorageBackend = (*memStorage)(nil) func setupFileService(t *testing.T) *FileService { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatalf("open db: %v", err) } if err := db.AutoMigrate(&model.File{}); err != nil { t.Fatalf("migrate: %v", err) } fileRepo := repository.NewFileRepository(db) store := newMemStorage() return NewFileService(fileRepo, store, 0) // 0 = unlimited } func TestFileService_Upload(t *testing.T) { svc := setupFileService(t) ctx := context.Background() info, err := svc.Upload(ctx, "user1", nil, "hello.txt", strings.NewReader("hello world")) if err != nil { t.Fatalf("Upload = %v", err) } if info.ID == "" { t.Fatal("file ID is empty") } if info.Name != "hello.txt" { t.Errorf("Name = %q, want %q", info.Name, "hello.txt") } if info.Size != 11 { t.Errorf("Size = %d, want 11", info.Size) } if info.UserID != "user1" { t.Errorf("UserID = %q, want %q", info.UserID, "user1") } if info.IsDir { t.Error("IsDir should be false") } // SHA-256 of "hello world" = b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 expectedHash := "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" if info.Hash != expectedHash { t.Errorf("Hash = %q, want %q", info.Hash, expectedHash) } } func TestFileService_UploadIntoDirectory(t *testing.T) { svc := setupFileService(t) ctx := context.Background() dir, err := svc.CreateDir(ctx, "user1", nil, "docs") if err != nil { t.Fatalf("CreateDir = %v", err) } info, err := svc.Upload(ctx, "user1", &dir.ID, "readme.txt", strings.NewReader("README")) if err != nil { t.Fatalf("Upload = %v", err) } if info.ParentID == nil || *info.ParentID != dir.ID { t.Error("file should be inside the docs directory") } } func TestFileService_UploadInvalidName(t *testing.T) { svc := setupFileService(t) ctx := context.Background() invalidNames := []string{"", "foo/bar", "foo\\bar", ".", "..", "foo\x00bar"} for _, name := range invalidNames { _, err := svc.Upload(ctx, "user1", nil, name, strings.NewReader("data")) if err == nil { t.Errorf("expected error for name %q, got nil", name) } } } func TestFileService_UploadDuplicateName(t *testing.T) { svc := setupFileService(t) ctx := context.Background() _, err := svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("first")) if err != nil { t.Fatalf("first Upload = %v", err) } _, err = svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("second")) if err == nil { t.Fatal("expected duplicate name error, got nil") } } func TestFileService_UploadNonexistentParent(t *testing.T) { svc := setupFileService(t) ctx := context.Background() fakeParentID := "nonexistent-id" _, err := svc.Upload(ctx, "user1", &fakeParentID, "file.txt", strings.NewReader("data")) if err == nil { t.Fatal("expected error for nonexistent parent, got nil") } } func TestFileService_UploadParentNotDirectory(t *testing.T) { svc := setupFileService(t) ctx := context.Background() file, err := svc.Upload(ctx, "user1", nil, "not-a-dir.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } _, err = svc.Upload(ctx, "user1", &file.ID, "child.txt", strings.NewReader("data")) if err == nil { t.Fatal("expected error for non-directory parent, got nil") } } func TestFileService_Download(t *testing.T) { svc := setupFileService(t) ctx := context.Background() content := "download test content" info, err := svc.Upload(ctx, "user1", nil, "download.txt", strings.NewReader(content)) if err != nil { t.Fatalf("Upload = %v", err) } reader, dlInfo, err := svc.Download(ctx, "user1", info.ID) if err != nil { t.Fatalf("Download = %v", err) } defer reader.Close() if dlInfo.Name != "download.txt" { t.Errorf("Name = %q, want %q", dlInfo.Name, "download.txt") } data, err := io.ReadAll(reader) if err != nil { t.Fatalf("ReadAll = %v", err) } if string(data) != content { t.Errorf("content = %q, want %q", string(data), content) } } func TestFileService_DownloadForbidden(t *testing.T) { svc := setupFileService(t) ctx := context.Background() info, err := svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } _, _, err = svc.Download(ctx, "user2", info.ID) if err != model.ErrForbidden { t.Errorf("expected ErrForbidden, got %v", err) } } func TestFileService_DownloadDirectory(t *testing.T) { svc := setupFileService(t) ctx := context.Background() dir, err := svc.CreateDir(ctx, "user1", nil, "mydir") if err != nil { t.Fatalf("CreateDir = %v", err) } _, _, err = svc.Download(ctx, "user1", dir.ID) if err == nil { t.Fatal("expected error downloading directory, got nil") } } func TestFileService_Get(t *testing.T) { svc := setupFileService(t) ctx := context.Background() created, err := svc.Upload(ctx, "user1", nil, "info.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } info, err := svc.Get(ctx, "user1", created.ID) if err != nil { t.Fatalf("Get = %v", err) } if info.ID != created.ID { t.Errorf("ID = %q, want %q", info.ID, created.ID) } } func TestFileService_GetNotFound(t *testing.T) { svc := setupFileService(t) ctx := context.Background() _, err := svc.Get(ctx, "user1", "nonexistent") if err != model.ErrNotFound { t.Errorf("expected ErrNotFound, got %v", err) } } func TestFileService_GetForbidden(t *testing.T) { svc := setupFileService(t) ctx := context.Background() info, err := svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } _, err = svc.Get(ctx, "user2", info.ID) if err != model.ErrForbidden { t.Errorf("expected ErrForbidden, got %v", err) } } func TestFileService_List(t *testing.T) { svc := setupFileService(t) ctx := context.Background() dir, err := svc.CreateDir(ctx, "user1", nil, "dir") if err != nil { t.Fatalf("CreateDir = %v", err) } _, err = svc.Upload(ctx, "user1", nil, "root.txt", strings.NewReader("root")) if err != nil { t.Fatalf("Upload root = %v", err) } _, err = svc.Upload(ctx, "user1", &dir.ID, "nested.txt", strings.NewReader("nested")) if err != nil { t.Fatalf("Upload nested = %v", err) } // List root. rootList, err := svc.List(ctx, "user1", nil, 0, 50) if err != nil { t.Fatalf("List root = %v", err) } if rootList.Total != 2 { t.Errorf("root total = %d, want 2", rootList.Total) } // List inside directory. dirList, err := svc.List(ctx, "user1", &dir.ID, 0, 50) if err != nil { t.Fatalf("List dir = %v", err) } if dirList.Total != 1 { t.Errorf("dir total = %d, want 1", dirList.Total) } } func TestFileService_Update(t *testing.T) { svc := setupFileService(t) ctx := context.Background() info, err := svc.Upload(ctx, "user1", nil, "oldname.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } updated, err := svc.Update(ctx, "user1", info.ID, "newname.txt", nil) if err != nil { t.Fatalf("Update = %v", err) } if updated.Name != "newname.txt" { t.Errorf("Name = %q, want %q", updated.Name, "newname.txt") } } func TestFileService_UpdateMove(t *testing.T) { svc := setupFileService(t) ctx := context.Background() dir1, err := svc.CreateDir(ctx, "user1", nil, "dir1") if err != nil { t.Fatalf("CreateDir 1 = %v", err) } dir2, err := svc.CreateDir(ctx, "user1", nil, "dir2") if err != nil { t.Fatalf("CreateDir 2 = %v", err) } info, err := svc.Upload(ctx, "user1", &dir1.ID, "move.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } updated, err := svc.Update(ctx, "user1", info.ID, "", &dir2.ID) if err != nil { t.Fatalf("Update = %v", err) } if updated.ParentID == nil || *updated.ParentID != dir2.ID { t.Error("file should be moved to dir2") } } func TestFileService_Delete(t *testing.T) { svc := setupFileService(t) ctx := context.Background() info, err := svc.Upload(ctx, "user1", nil, "delete-me.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } if err := svc.Delete(ctx, "user1", info.ID); err != nil { t.Fatalf("Delete = %v", err) } _, err = svc.Get(ctx, "user1", info.ID) if err != model.ErrNotFound { t.Errorf("expected ErrNotFound after delete, got %v", err) } } func TestFileService_DeleteNonEmptyDirectory(t *testing.T) { svc := setupFileService(t) ctx := context.Background() dir, err := svc.CreateDir(ctx, "user1", nil, "dir") if err != nil { t.Fatalf("CreateDir = %v", err) } _, err = svc.Upload(ctx, "user1", &dir.ID, "child.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } err = svc.Delete(ctx, "user1", dir.ID) if err == nil { t.Fatal("expected error deleting non-empty directory, got nil") } } func TestFileService_DeleteForbidden(t *testing.T) { svc := setupFileService(t) ctx := context.Background() info, err := svc.Upload(ctx, "user1", nil, "file.txt", strings.NewReader("data")) if err != nil { t.Fatalf("Upload = %v", err) } err = svc.Delete(ctx, "user2", info.ID) if err != model.ErrForbidden { t.Errorf("expected ErrForbidden, got %v", err) } } func TestFileService_CreateDir(t *testing.T) { svc := setupFileService(t) ctx := context.Background() dir, err := svc.CreateDir(ctx, "user1", nil, "mydir") if err != nil { t.Fatalf("CreateDir = %v", err) } if !dir.IsDir { t.Error("IsDir should be true") } if dir.Name != "mydir" { t.Errorf("Name = %q, want %q", dir.Name, "mydir") } } func TestFileService_CreateDirDuplicateName(t *testing.T) { svc := setupFileService(t) ctx := context.Background() _, err := svc.CreateDir(ctx, "user1", nil, "dup") if err != nil { t.Fatalf("first CreateDir = %v", err) } _, err = svc.CreateDir(ctx, "user1", nil, "dup") if err == nil { t.Fatal("expected duplicate name error, got nil") } } func TestFileService_VerifyParentForbidden(t *testing.T) { svc := setupFileService(t) ctx := context.Background() dir, err := svc.CreateDir(ctx, "user1", nil, "dir") if err != nil { t.Fatalf("CreateDir = %v", err) } _, err = svc.Upload(ctx, "user2", &dir.ID, "stolen.txt", strings.NewReader("data")) if err != model.ErrForbidden { t.Errorf("expected ErrForbidden, got %v", err) } }