package filesvc_test import ( "bytes" "context" "errors" "io" "testing" "github.com/stretchr/testify/require" "portal-mini-go/internal/model" "portal-mini-go/internal/pkg/config" "portal-mini-go/internal/pkg/storage" filesvc "portal-mini-go/internal/service/file" ) // --- 测试替身 --- type fakeStorage struct { putErr error gotKey string gotSize int64 data map[string][]byte } func newFakeStorage() *fakeStorage { return &fakeStorage{data: map[string][]byte{}} } func (s *fakeStorage) Put(_ context.Context, key string, r io.Reader, size int64) error { if s.putErr != nil { return s.putErr } b, _ := io.ReadAll(r) s.data[key] = b s.gotKey = key s.gotSize = size return nil } func (s *fakeStorage) Get(_ context.Context, key string) (io.ReadCloser, error) { b, ok := s.data[key] if !ok { return nil, errors.New("not found") } return io.NopCloser(bytes.NewReader(b)), nil } func (s *fakeStorage) Delete(_ context.Context, key string) error { delete(s.data, key) return nil } func (s *fakeStorage) URL(key string) string { return "/files/static/" + key } type fakeRepo struct { created *model.File byID *model.File getErr error createErr error delErr error deletedID uint64 } func (r *fakeRepo) Create(_ context.Context, f *model.File) error { r.created = f; return r.createErr } func (r *fakeRepo) GetByID(_ context.Context, id uint64) (*model.File, error) { if r.getErr != nil { return nil, r.getErr } return r.byID, nil } func (r *fakeRepo) Delete(_ context.Context, id uint64) error { r.deletedID = id; return r.delErr } // 编译期接口断言。 var ( _ filesvc.FileRepository = (*fakeRepo)(nil) _ storage.Storage = (*fakeStorage)(nil) ) func newSvc(cfg config.UploadConfig, store *fakeStorage, repo *fakeRepo) *filesvc.Service { return filesvc.New(repo, store, cfg) } // --- Upload --- func TestUpload_Success(t *testing.T) { store := newFakeStorage() repo := &fakeRepo{} svc := newSvc(config.UploadConfig{MaxSize: 1 << 20}, store, repo) res, err := svc.Upload(context.Background(), filesvc.UploadInput{ Reader: bytes.NewReader([]byte("hello")), Size: 5, Filename: "我的图片.png", UploaderType: "admin", UploaderID: 1, }) require.NoError(t, err) require.NotNil(t, res) require.Equal(t, uint64(1), repo.created.UploaderID) require.Equal(t, "admin", repo.created.UploaderType) require.Equal(t, "png", repo.created.Ext) require.Equal(t, "image/png", repo.created.MimeType) require.NotEmpty(t, repo.created.StorageKey) require.Equal(t, store.URL(repo.created.StorageKey), res.URL) require.Contains(t, store.gotKey, ".png") } func TestUpload_TooLarge(t *testing.T) { svc := newSvc(config.UploadConfig{MaxSize: 10}, newFakeStorage(), &fakeRepo{}) _, err := svc.Upload(context.Background(), filesvc.UploadInput{ Reader: bytes.NewReader(make([]byte, 11)), Size: 11, Filename: "a.png", }) require.ErrorIs(t, err, filesvc.ErrFileTooLarge) } func TestUpload_TypeForbidden(t *testing.T) { svc := newSvc(config.UploadConfig{MaxSize: 1 << 20, AllowedExt: []string{"jpg"}}, newFakeStorage(), &fakeRepo{}) _, err := svc.Upload(context.Background(), filesvc.UploadInput{ Reader: bytes.NewReader([]byte("x")), Size: 1, Filename: "a.png", }) require.ErrorIs(t, err, filesvc.ErrFileTypeForbid) } func TestUpload_EmptyWhitelistAllowsAll(t *testing.T) { store := newFakeStorage() repo := &fakeRepo{} svc := newSvc(config.UploadConfig{MaxSize: 1 << 20}, store, repo) _, err := svc.Upload(context.Background(), filesvc.UploadInput{ Reader: bytes.NewReader([]byte("x")), Size: 1, Filename: "a.unknown-ext", }) require.NoError(t, err) require.Equal(t, "unknown-ext", repo.created.Ext) } func TestUpload_StoreErrorNoCreate(t *testing.T) { store := newFakeStorage() store.putErr = errors.New("disk full") repo := &fakeRepo{} svc := newSvc(config.UploadConfig{}, store, repo) _, err := svc.Upload(context.Background(), filesvc.UploadInput{ Reader: bytes.NewReader([]byte("x")), Size: 1, Filename: "a.png", }) require.Error(t, err) require.Nil(t, repo.created, "存储失败不应写入元数据") } func TestUpload_RollbackOnRepoError(t *testing.T) { store := newFakeStorage() repo := &fakeRepo{createErr: errors.New("db down")} svc := newSvc(config.UploadConfig{}, store, repo) _, err := svc.Upload(context.Background(), filesvc.UploadInput{ Reader: bytes.NewReader([]byte("x")), Size: 1, Filename: "a.png", }) require.Error(t, err) require.Empty(t, store.data, "元数据落库失败应回滚已落盘文件") } // --- Get / Download / Delete --- func TestGet(t *testing.T) { want := &model.File{ID: 7, OriginName: "doc.pdf"} svc := newSvc(config.UploadConfig{}, newFakeStorage(), &fakeRepo{byID: want}) got, err := svc.Get(context.Background(), 7) require.NoError(t, err) require.Equal(t, want, got) } func TestDownload(t *testing.T) { store := newFakeStorage() store.data["2006/01/01/abc.png"] = []byte("binary") repo := &fakeRepo{byID: &model.File{ID: 3, StorageKey: "2006/01/01/abc.png", MimeType: "image/png", Size: 6}} svc := newSvc(config.UploadConfig{}, store, repo) rc, f, err := svc.Download(context.Background(), 3) require.NoError(t, err) defer rc.Close() b, _ := io.ReadAll(rc) require.Equal(t, []byte("binary"), b) require.Equal(t, "image/png", f.MimeType) } func TestDelete(t *testing.T) { store := newFakeStorage() store.data["k"] = []byte("x") repo := &fakeRepo{byID: &model.File{ID: 9, StorageKey: "k"}} svc := newSvc(config.UploadConfig{}, store, repo) require.NoError(t, svc.Delete(context.Background(), 9)) require.Equal(t, uint64(9), repo.deletedID) require.Empty(t, store.data) }