spark-server/internal/module/user/logic/login_test.go
2026-09-18 17:14:38 +08:00

204 lines
6.0 KiB
Go

package logic
import (
"bytes"
"context"
"errors"
"image/png"
"strconv"
"strings"
"testing"
"spark-server/internal/infrastructure/id"
"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.DeviceType != "ios" || dev.LastIP != "203.0.113.10" || dev.AppVersion != "" {
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)
}
dev, err = repo.GetDevice(ctx, u1.ID, 7)
if err != nil {
t.Fatal(err)
}
if dev.AppVersion != "1.0.0" || dev.Language != "zh-CN" {
t.Fatalf("%+v", dev)
}
logs := repo.LoginLogs()
if len(logs) < 2 || logs[0].DeviceType != entity.DeviceIOS || logs[0].DeviceID != 7 {
t.Fatalf("logs %+v", logs)
}
}
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.ErrUnauthorized) {
t.Fatalf("no code: %v", err)
}
if err := codes.Put(ctx, "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.ErrUnauthorized) {
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)
}
}
func TestRegisterFallsBackToDataURIWithoutObjectStore(t *testing.T) {
ids, err := id.New(1)
if err != nil {
t.Fatal(err)
}
svc := NewService(model.NewMemoryRepo(), model.NewMemoryCodes(), 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)
}
svc := NewService(model.NewMemoryRepo(), model.NewMemoryCodes(), 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")
}