优化结构
This commit is contained in:
@@ -1,250 +0,0 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type listStorage struct {
|
||||
items []*StoredFile
|
||||
limits []int
|
||||
}
|
||||
|
||||
func (*listStorage) Put(context.Context, string, io.Reader) (*StoredFile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (*listStorage) Open(context.Context, string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
}
|
||||
func (*listStorage) Delete(context.Context, string) error { return nil }
|
||||
func (*listStorage) Compose(context.Context, []string, string) (*StoredFile, string, error) {
|
||||
return nil, "", nil
|
||||
}
|
||||
func (*listStorage) DeletePrefix(context.Context, string) error { return nil }
|
||||
func (s *listStorage) List(_ context.Context, _ string, cursor string, limit int) ([]*StoredFile, string, bool, error) {
|
||||
s.limits = append(s.limits, limit)
|
||||
start := 0
|
||||
for start < len(s.items) && s.items[start].Path <= cursor {
|
||||
start++
|
||||
}
|
||||
end := start + limit
|
||||
if end > len(s.items) {
|
||||
end = len(s.items)
|
||||
}
|
||||
page := s.items[start:end]
|
||||
more := end < len(s.items)
|
||||
next := ""
|
||||
if len(page) > 0 {
|
||||
next = page[len(page)-1].Path
|
||||
}
|
||||
return page, next, more, nil
|
||||
}
|
||||
|
||||
func TestListStorageFiltersChunksWithoutSkippingVisibleObjects(t *testing.T) {
|
||||
storage := &listStorage{items: []*StoredFile{
|
||||
{Path: "uploads/chunks/1/00000000"},
|
||||
{Path: "uploads/chunks/1/00000001"},
|
||||
{Path: "visible/a"},
|
||||
{Path: "visible/b"},
|
||||
{Path: "visible/c"},
|
||||
{Path: "visible/d"},
|
||||
{Path: "visible/e"},
|
||||
}}
|
||||
uc := NewMediaUsecase(nil, storage, nil)
|
||||
|
||||
first, cursor, more, err := uc.ListStorage(context.Background(), "", "", 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := []string{first[0].Path, first[1].Path, first[2].Path}; strings.Join(got, ",") != "visible/a,visible/b,visible/c" {
|
||||
t.Fatalf("first page = %v", got)
|
||||
}
|
||||
if cursor != "visible/c" || !more {
|
||||
t.Fatalf("first cursor/more = %q/%v", cursor, more)
|
||||
}
|
||||
if len(storage.limits) != 2 || storage.limits[0] != 3 || storage.limits[1] != 2 {
|
||||
t.Fatalf("storage limits = %v, want [3 2]", storage.limits)
|
||||
}
|
||||
|
||||
second, cursor, more, err := uc.ListStorage(context.Background(), "", cursor, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := []string{second[0].Path, second[1].Path}; strings.Join(got, ",") != "visible/d,visible/e" {
|
||||
t.Fatalf("second page = %v", got)
|
||||
}
|
||||
if cursor != "visible/e" || more {
|
||||
t.Fatalf("second cursor/more = %q/%v", cursor, more)
|
||||
}
|
||||
}
|
||||
|
||||
type mediaLimitSettings struct {
|
||||
RuntimeSettings
|
||||
max int64
|
||||
}
|
||||
|
||||
func (s mediaLimitSettings) MediaSettings() MediaSettings {
|
||||
return MediaSettings{MaxFileSize: s.max}
|
||||
}
|
||||
|
||||
type mediaLimitStorage struct {
|
||||
readSize int64
|
||||
reportedSize *int64
|
||||
deleted bool
|
||||
composeCalled bool
|
||||
composed *StoredFile
|
||||
composedHash string
|
||||
}
|
||||
|
||||
func (s *mediaLimitStorage) Put(_ context.Context, _ string, reader io.Reader) (*StoredFile, error) {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.readSize = int64(len(data))
|
||||
size := s.readSize
|
||||
if s.reportedSize != nil {
|
||||
size = *s.reportedSize
|
||||
}
|
||||
return &StoredFile{Size: size}, nil
|
||||
}
|
||||
func (*mediaLimitStorage) Open(context.Context, string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
}
|
||||
func (s *mediaLimitStorage) Delete(context.Context, string) error {
|
||||
s.deleted = true
|
||||
return nil
|
||||
}
|
||||
func (s *mediaLimitStorage) Compose(context.Context, []string, string) (*StoredFile, string, error) {
|
||||
s.composeCalled = true
|
||||
return s.composed, s.composedHash, nil
|
||||
}
|
||||
func (*mediaLimitStorage) DeletePrefix(context.Context, string) error { return nil }
|
||||
func (*mediaLimitStorage) List(context.Context, string, string, int) ([]*StoredFile, string, bool, error) {
|
||||
return nil, "", false, nil
|
||||
}
|
||||
|
||||
type mediaUploadRepo struct {
|
||||
MediaRepo
|
||||
session *UploadSession
|
||||
chunks []*UploadChunk
|
||||
claimed bool
|
||||
failed bool
|
||||
}
|
||||
|
||||
func (r *mediaUploadRepo) FindUploadSession(context.Context, uint) (*UploadSession, error) {
|
||||
if r.session == nil {
|
||||
return nil, ErrUploadSessionNotFound
|
||||
}
|
||||
return r.session, nil
|
||||
}
|
||||
func (r *mediaUploadRepo) ClaimUploadSession(context.Context, uint) (bool, error) {
|
||||
return r.claimed, nil
|
||||
}
|
||||
func (r *mediaUploadRepo) FailUploadSession(context.Context, uint) error {
|
||||
r.failed = true
|
||||
return nil
|
||||
}
|
||||
func (r *mediaUploadRepo) ListChunks(context.Context, uint) ([]*UploadChunk, error) {
|
||||
return r.chunks, nil
|
||||
}
|
||||
|
||||
func md5Text(value string) string {
|
||||
sum := md5.Sum([]byte(value))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func TestUploadRejectsDataPastConfiguredLimit(t *testing.T) {
|
||||
storage := &mediaLimitStorage{}
|
||||
uc := NewMediaUsecase(&mediaUploadRepo{}, storage, mediaLimitSettings{max: 4})
|
||||
|
||||
_, err := uc.Upload(context.Background(), 1, "sample.txt", "text/plain", 0, strings.NewReader("12345"), false)
|
||||
if !errors.Is(err, ErrMediaTooLarge) {
|
||||
t.Fatalf("Upload() error = %v, want ErrMediaTooLarge", err)
|
||||
}
|
||||
if storage.readSize != 5 || !storage.deleted {
|
||||
t.Fatalf("storage read/deleted = %d/%v, want 5/true", storage.readSize, storage.deleted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadRejectsStorageSizeMismatch(t *testing.T) {
|
||||
reported := int64(3)
|
||||
storage := &mediaLimitStorage{reportedSize: &reported}
|
||||
uc := NewMediaUsecase(&mediaUploadRepo{}, storage, mediaLimitSettings{max: 4})
|
||||
|
||||
if _, err := uc.Upload(context.Background(), 1, "sample.txt", "text/plain", 0, strings.NewReader("1234"), false); err == nil {
|
||||
t.Fatal("Upload() error = nil, want storage size mismatch")
|
||||
}
|
||||
if !storage.deleted {
|
||||
t.Fatal("mismatched stored object was not deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitUploadRejectsInvalidLayout(t *testing.T) {
|
||||
uc := NewMediaUsecase(&mediaUploadRepo{}, &mediaLimitStorage{}, mediaLimitSettings{max: 10})
|
||||
validHash := strings.Repeat("0", md5.Size*2)
|
||||
tests := []struct {
|
||||
name string
|
||||
hash string
|
||||
size int64
|
||||
chunkSize int64
|
||||
total int
|
||||
wantLarge bool
|
||||
}{
|
||||
{name: "invalid hash", hash: "bad", size: 5, chunkSize: 3, total: 2},
|
||||
{name: "zero size", hash: validHash, size: 0, chunkSize: 3, total: 1},
|
||||
{name: "over limit", hash: validHash, size: 11, chunkSize: 3, total: 4, wantLarge: true},
|
||||
{name: "wrong total", hash: validHash, size: 5, chunkSize: 3, total: 3},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, _, _, err := uc.InitUpload(context.Background(), 1, "sample.txt", test.hash, test.size, test.chunkSize, test.total)
|
||||
if err == nil {
|
||||
t.Fatal("InitUpload() error = nil")
|
||||
}
|
||||
if test.wantLarge && !errors.Is(err, ErrMediaTooLarge) {
|
||||
t.Fatalf("InitUpload() error = %v, want ErrMediaTooLarge", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveChunkRejectsIndexAndSizeMismatch(t *testing.T) {
|
||||
repo := &mediaUploadRepo{session: &UploadSession{ID: 7, UserID: 1, FileSize: 5, ChunkSize: 3, ChunkTotal: 2, Status: "uploading"}}
|
||||
uc := NewMediaUsecase(repo, &mediaLimitStorage{}, mediaLimitSettings{max: 10})
|
||||
|
||||
if err := uc.SaveChunk(context.Background(), 1, 7, 2, md5Text("abc"), strings.NewReader("abc")); err == nil {
|
||||
t.Fatal("SaveChunk() accepted an out-of-range index")
|
||||
}
|
||||
if err := uc.SaveChunk(context.Background(), 1, 7, 0, md5Text("ab"), strings.NewReader("ab")); err == nil {
|
||||
t.Fatal("SaveChunk() accepted a short chunk")
|
||||
}
|
||||
if err := uc.SaveChunk(context.Background(), 1, 7, 0, md5Text("abcd"), strings.NewReader("abcd")); err == nil {
|
||||
t.Fatal("SaveChunk() accepted an oversized chunk")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteUploadRejectsMismatchedChunkSizesBeforeCompose(t *testing.T) {
|
||||
repo := &mediaUploadRepo{
|
||||
session: &UploadSession{ID: 7, UserID: 1, FileName: "sample.txt", FileHash: md5Text("abcde"), FileSize: 5, ChunkSize: 3, ChunkTotal: 2, Status: "uploading"},
|
||||
chunks: []*UploadChunk{{Index: 0, Size: 3}, {Index: 1, Size: 1}},
|
||||
claimed: true,
|
||||
}
|
||||
storage := &mediaLimitStorage{}
|
||||
uc := NewMediaUsecase(repo, storage, mediaLimitSettings{max: 10})
|
||||
|
||||
if _, err := uc.CompleteUpload(context.Background(), 1, 7, "text/plain"); err == nil {
|
||||
t.Fatal("CompleteUpload() accepted mismatched chunk sizes")
|
||||
}
|
||||
if storage.composeCalled {
|
||||
t.Fatal("Compose() was called before chunk-size validation completed")
|
||||
}
|
||||
if !repo.failed {
|
||||
t.Fatal("failed upload session was not returned to the uploading state")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user