package logic import ( "context" "errors" "strings" "testing" "time" "spark-server/internal/module/upload/entity" "spark-server/internal/pkg/errs" "spark-server/internal/pkg/reqctx" ) type fakeStore struct { put, get string putErr error } func (f *fakeStore) PresignPut(_ context.Context, key, _ string, _ int64, _ time.Duration) (string, error) { if f.putErr != nil { return "", f.putErr } if f.put != "" { return f.put, nil } return "http://rustfs.test/" + key + "?sig=1", nil } func (f *fakeStore) PresignGet(_ context.Context, key string, _ time.Duration) (string, error) { if f.get != "" { return f.get, nil } return "http://rustfs.test/" + key + "?get=1", nil } func testUpload(t *testing.T) *Service { t.Helper() return NewService(&fakeStore{}, Options{ TTL: 5 * time.Minute, Now: func() time.Time { return time.Date(2026, 9, 17, 8, 0, 0, 0, time.UTC) }, }) } func TestCreateCredentialChatVsAvatar(t *testing.T) { svc := testUpload(t) ctx := reqctx.With(t.Context(), &reqctx.RequestContext{UserID: 42, Language: "zh-CN"}) chat, err := svc.CreateCredential(ctx, 42, &CreateInput{Purpose: entity.PurposeChatImage, Mime: "image/jpeg", Size: 1024}) if err != nil { t.Fatal(err) } if !strings.HasPrefix(chat.ObjectKey, "chat/image/20260917/") || chat.Method != "PUT" || chat.Headers["Content-Type"] != "image/jpeg" || chat.Headers["Content-Length"] != "1024" || chat.Headers["If-None-Match"] != "*" || strings.Contains(chat.ObjectKey, "/42/") { t.Fatalf("%+v", chat) } avatar, err := svc.CreateCredential(ctx, 42, &CreateInput{Purpose: entity.PurposeAvatar, Mime: "image/png", Size: 2048}) if err != nil || !strings.HasPrefix(avatar.ObjectKey, "perm/avatar/20260917/") { t.Fatalf("%+v %v", avatar, err) } group, err := svc.CreateCredential(ctx, 42, &CreateInput{Purpose: entity.PurposeGroupAvatar, Mime: "image/jpeg", Size: 1024, GroupID: 7}) if err != nil || !strings.HasPrefix(group.ObjectKey, "perm/group/7/") { t.Fatalf("%+v %v", group, err) } if _, err := svc.CreateCredential(ctx, 42, &CreateInput{Purpose: entity.PurposeMomentImage, Mime: "image/gif", Size: 2048}); err != nil { t.Fatalf("moment gif %v", err) } if _, err := svc.CreateCredential(ctx, 42, &CreateInput{Purpose: entity.PurposeGroupAvatar, Mime: "image/jpeg", Size: 1024}); err == nil { t.Fatal("group avatar without group_id") } } func TestCreateCredentialRejectsBadMIMEAndSize(t *testing.T) { svc := testUpload(t) ctx := reqctx.With(t.Context(), &reqctx.RequestContext{UserID: 1, Language: "zh-CN"}) if _, err := svc.CreateCredential(ctx, 1, &CreateInput{Purpose: entity.PurposeAvatar, Mime: "application/pdf", Size: 100}); err == nil { t.Fatal("pdf avatar") } if _, err := svc.CreateCredential(ctx, 1, &CreateInput{Purpose: entity.PurposeChatImage, Mime: "image/jpeg", Size: 50 << 20}); err == nil { t.Fatal("huge image") } for _, mime := range []string{"image/heic", "image/heif", "image/webp"} { if _, err := svc.CreateCredential(ctx, 1, &CreateInput{Purpose: entity.PurposeChatImage, Mime: mime, Size: 1024}); err == nil { t.Fatalf("accepted %s", mime) } } if _, err := svc.CreateCredential(ctx, 0, &CreateInput{Purpose: entity.PurposeChatImage, Mime: "image/jpeg", Size: 1}); !errors.Is(err, errs.ErrUnauthorized) { t.Fatalf("uid %v", err) } } func TestCreateDownloadValidatesKey(t *testing.T) { svc := testUpload(t) ctx := reqctx.With(t.Context(), &reqctx.RequestContext{UserID: 1, Language: "en"}) got, err := svc.CreateDownload(ctx, 1, &DownloadInput{ObjectKey: "chat/20260917/1/2.jpg"}) if err != nil || got.Method != "GET" || !strings.Contains(got.DownloadURL, "chat/20260917/1/2.jpg") { t.Fatalf("%+v %v", got, err) } if _, err := svc.CreateDownload(ctx, 1, &DownloadInput{ObjectKey: "../secret"}); err == nil { t.Fatal("path escape") } } func TestCreateCredentialsBatch(t *testing.T) { svc := testUpload(t) ctx := reqctx.With(t.Context(), &reqctx.RequestContext{UserID: 42, Language: "zh-CN"}) part := func(size int64) CreateInput { return CreateInput{Purpose: entity.PurposeChatImage, Mime: "image/jpeg", Size: size} } got, err := svc.CreateCredentials(ctx, 42, &BatchCreateInput{Items: []CreateInput{part(1024), part(2048), part(4096)}}) if err != nil || got == nil || len(got.Items) != 3 { t.Fatalf("%+v %v", got, err) } if got.Items[0].ObjectKey == got.Items[1].ObjectKey || got.Items[1].Headers["Content-Length"] != "2048" { t.Fatalf("%+v", got.Items) } if _, err := svc.CreateCredentials(ctx, 42, &BatchCreateInput{}); err == nil { t.Fatal("empty") } if _, err := svc.CreateCredentials(ctx, 42, &BatchCreateInput{Items: []CreateInput{part(1), part(1), part(1), part(1)}}); err == nil { t.Fatal("too many") } }