package logic import ( "bytes" "context" "errors" "fmt" "image/png" "strconv" "strings" "testing" "spark-server/internal/infrastructure/id" smslogic "spark-server/internal/module/sms/logic" smsmodel "spark-server/internal/module/sms/model" smsentity "spark-server/internal/module/sms/entity" "spark-server/internal/module/user/entity" "spark-server/internal/module/user/model" "spark-server/internal/pkg/errs" "spark-server/internal/pkg/reqctx" ) func TestAuthenticateTestEmailAnyCode(t *testing.T) { svc, repo, _ := testService(t) ctx := reqctx.With(t.Context(), &reqctx.RequestContext{RealIP: "203.0.113.10", Language: "zh-CN", AppVersion: "1.0.0"}) device := &entity.LoginDevice{DeviceID: 7, Platform: "ios", DeviceName: "iPhone"} u1, err := svc.AuthenticateEmail(ctx, "test01@spark.com", "whatever", device) if err != nil { t.Fatal(err) } if u1.ID <= 0 || !entity.ValidSparkID(u1.SparkID) || u1.Nickname != "test01" || !entity.ValidQRCode(u1.QRCode) { t.Fatalf("%+v", u1) } if !strings.HasPrefix(u1.Avatar, "perm/avatar/") || !strings.HasSuffix(u1.Avatar, ".png") { t.Fatalf("default avatar %s", u1.Avatar) } uid := strconv.FormatInt(u1.ID, 10) for _, part := range strings.Split(u1.Avatar, "/") { if part == uid { t.Fatalf("avatar leaked uid %s", u1.Avatar) } } store, ok := svc.objects.(*memObjects) if !ok { t.Fatal("mem objects") } if _, err := png.Decode(bytes.NewReader(store.kv[u1.Avatar])); err != nil { t.Fatalf("avatar png %v", err) } if strings.Contains(u1.QRCode, strconv.FormatInt(u1.ID, 10)) { t.Fatalf("qr leaked uid %s", u1.QRCode) } got, err := svc.GetByQRCode(ctx, u1.QRCode) if err != nil || got.ID != u1.ID { t.Fatalf("lookup qr %v %+v", err, got) } if u1.LastLoginAt.IsZero() || u1.LastActiveAt.IsZero() || u1.Gender != entity.GenderUnknown { t.Fatalf("profile times/gender %+v", u1) } u2, err := svc.AuthenticateEmail(ctx, "TEST01@spark.com", "0000", device) if err != nil { t.Fatal(err) } if u2.ID != u1.ID { t.Fatalf("same user %d %d", u1.ID, u2.ID) } dev, err := repo.GetDevice(ctx, u1.ID, 7) if err != nil { t.Fatal(err) } if dev.DeviceID != 7 || dev.LastLoginAt.IsZero() { t.Fatalf("%+v", dev) } device.AppVersion = "1.0.0" device.RealIP = "203.0.113.10" device.Language = "zh-CN" if _, err := svc.AuthenticateEmail(ctx, "test01@spark.com", "x", device); err != nil { t.Fatal(err) } logs := repo.LoginLogs() if len(logs) < 2 || logs[len(logs)-1].DeviceType != entity.DeviceIOS || logs[len(logs)-1].DeviceID != 7 { t.Fatalf("logs %+v", logs) } if logs[len(logs)-1].AppVersion != "1.0.0" || logs[len(logs)-1].Language != "zh-CN" { t.Fatalf("log context %+v", logs[len(logs)-1]) } } func TestStaleUIDQRCodeIsReplaced(t *testing.T) { svc, repo, _ := testService(t) ctx := t.Context() device := &entity.LoginDevice{DeviceID: 1, DeviceType: entity.DeviceIOS} u, err := svc.AuthenticateEmail(ctx, "test08@spark.com", "1", device) if err != nil { t.Fatal(err) } if err := repo.UpdateQRCode(ctx, u.ID, "spark:u:"+strconv.FormatInt(u.ID, 10)); err != nil { t.Fatal(err) } u2, err := svc.AuthenticateEmail(ctx, "test08@spark.com", "1", device) if err != nil { t.Fatal(err) } if !entity.ValidQRCode(u2.QRCode) || strings.Contains(u2.QRCode, "spark:") { t.Fatalf("stale qr %s", u2.QRCode) } } func TestAuthenticateNonTestNeedsCode(t *testing.T) { svc, _, codes := testService(t) ctx := t.Context() device := &entity.LoginDevice{DeviceID: 1, DeviceType: entity.DeviceWeb} _, err := svc.AuthenticateEmail(ctx, "ada@example.com", "123456", device) if !errors.Is(err, errs.ErrVerifyCodeInvalid) { t.Fatalf("no code: %v", err) } if err := codes.Put(ctx, fmt.Sprintf("%d:%d:%s", smsentity.ChannelEmail, smsentity.PurposeLogin, "ada@example.com"), "123456", 60); err != nil { t.Fatal(err) } u, err := svc.AuthenticateEmail(ctx, "ada@example.com", "123456", device) if err != nil { t.Fatal(err) } if u.ID <= 0 { t.Fatal(u) } _, err = svc.AuthenticateEmail(ctx, "ada@example.com", "123456", device) if !errors.Is(err, errs.ErrVerifyCodeInvalid) { t.Fatalf("consumed: %v", err) } } func TestSnowflakeIDsIncrease(t *testing.T) { svc, _, _ := testService(t) ctx := t.Context() a, err := svc.AuthenticateEmail(ctx, "test02@spark.com", "1", &entity.LoginDevice{DeviceID: 1, DeviceType: entity.DeviceIOS}) if err != nil { t.Fatal(err) } b, err := svc.AuthenticateEmail(ctx, "test03@spark.com", "1", &entity.LoginDevice{DeviceID: 1, DeviceType: entity.DeviceIOS}) if err != nil { t.Fatal(err) } if b.ID <= a.ID { t.Fatalf("ids %d %d", a.ID, b.ID) } if a.SparkID == "" || a.SparkID == b.SparkID || !entity.ValidSparkID(a.SparkID) || !entity.ValidSparkID(b.SparkID) { t.Fatalf("spark_id %s %s", a.SparkID, b.SparkID) } if a.SparkID != entity.DefaultSparkID(a.ID) || b.SparkID != entity.DefaultSparkID(b.ID) { t.Fatalf("derived %s %s", a.SparkID, b.SparkID) } } func TestRegisterFallsBackToDataURIWithoutObjectStore(t *testing.T) { ids, err := id.New(1) if err != nil { t.Fatal(err) } store := smsmodel.NewMemoryStore() smsSvc := smslogic.NewService(store, nil, nil, nil) svc := NewService(model.NewMemoryRepo(), smsSvc, ids, nil) device := &entity.LoginDevice{DeviceID: 1, DeviceType: entity.DeviceIOS} u, err := svc.AuthenticateEmail(t.Context(), "test09@spark.com", "1", device) if err != nil { t.Fatal(err) } if !strings.HasPrefix(u.Avatar, "data:image/svg+xml;base64,") { t.Fatalf("fallback avatar %s", u.Avatar) } } func TestRegisterFallsBackWhenObjectPutFails(t *testing.T) { ids, err := id.New(1) if err != nil { t.Fatal(err) } store := smsmodel.NewMemoryStore() smsSvc := smslogic.NewService(store, nil, nil, nil) svc := NewService(model.NewMemoryRepo(), smsSvc, ids, failObjects{}) device := &entity.LoginDevice{DeviceID: 1, DeviceType: entity.DeviceIOS} u, err := svc.AuthenticateEmail(t.Context(), "test09@spark.com", "1", device) if err != nil { t.Fatal(err) } if !strings.HasPrefix(u.Avatar, "data:image/svg+xml;base64,") { t.Fatalf("fallback avatar %s", u.Avatar) } } func TestEmptyAvatarBackfilledOnLogin(t *testing.T) { svc, _, _ := testService(t) ctx := t.Context() device := &entity.LoginDevice{DeviceID: 1, DeviceType: entity.DeviceIOS} u, err := svc.AuthenticateEmail(ctx, "test10@spark.com", "1", device) if err != nil { t.Fatal(err) } empty := "" if _, err := svc.UpdateProfile(ctx, u.ID, UpdateProfileInput{Avatar: &empty}); err != nil { t.Fatal(err) } u2, err := svc.AuthenticateEmail(ctx, "test10@spark.com", "1", device) if err != nil || strings.TrimSpace(u2.Avatar) == "" { t.Fatalf("backfill %+v %v", u2, err) } } type failObjects struct{} func (failObjects) Put(context.Context, string, []byte, string) error { return errors.New("oss down") }