This commit is contained in:
Yvan
2026-08-18 15:04:07 +08:00
parent 10ccc005be
commit 1ef8ecb7b2
42 changed files with 3027 additions and 296 deletions
+169
View File
@@ -2,6 +2,9 @@ package biz
import (
"context"
"crypto/md5"
"encoding/hex"
"errors"
"io"
"strings"
"testing"
@@ -79,3 +82,169 @@ func TestListStorageFiltersChunksWithoutSkippingVisibleObjects(t *testing.T) {
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")
}
}