Add HTTP /v1 MVC, TCP send path, groups with server-side member-avatar collage, object-storage upload, and friends/favorites around the architecture-v3 layout. Co-authored-by: Cursor <cursoragent@cursor.com>
98 lines
3.3 KiB
Go
98 lines
3.3 KiB
Go
package logic
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"spark-server/internal/infrastructure/id"
|
|
"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, _ 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()
|
|
ids, err := id.New(1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return NewService(&fakeStore{}, ids, 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/20260917/42/") || chat.Method != "PUT" || chat.Headers["Content-Type"] != "image/jpeg" {
|
|
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/42/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.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")
|
|
}
|
|
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")
|
|
}
|
|
}
|