package logic import ( "context" "errors" "strconv" "testing" "spark-server/internal/infrastructure/id" convlogic "spark-server/internal/module/conversation/logic" convmodel "spark-server/internal/module/conversation/model" delivermodel "spark-server/internal/module/deliver/model" "spark-server/internal/module/friend/entity" friendmodel "spark-server/internal/module/friend/model" msgentity "spark-server/internal/module/message/entity" msgmodel "spark-server/internal/module/message/model" userentity "spark-server/internal/module/user/entity" userlogic "spark-server/internal/module/user/logic" "spark-server/internal/module/user/logictest" "spark-server/internal/pkg/errs" ) type captureGreeter struct { n int a, b int64 } func (c *captureGreeter) BecameFriends(_ context.Context, a, b int64) error { c.n++ c.a, c.b = a, b return nil } func testFriends(t *testing.T) (*Service, *userlogic.Service, *captureGreeter) { t.Helper() ids, err := id.New(1) if err != nil { t.Fatal(err) } users := logictest.Users(ids, nil) g := &captureGreeter{} return NewService(friendmodel.NewMemoryRepo(), users, ids, g), users, g } func twoUsers(t *testing.T, users *userlogic.Service) (*userentity.User, *userentity.User) { t.Helper() ctx := t.Context() a, err := users.AuthenticateEmail(ctx, "test01@spark.com", "1", &userentity.LoginDevice{DeviceID: 1, DeviceType: userentity.DeviceIOS}) if err != nil { t.Fatal(err) } b, err := users.AuthenticateEmail(ctx, "test02@spark.com", "1", &userentity.LoginDevice{DeviceID: 2, DeviceType: userentity.DeviceWeb}) if err != nil { t.Fatal(err) } return a, b } func TestApplyByEmailSparkIDQRAndDedupe(t *testing.T) { svc, users, _ := testFriends(t) ctx := t.Context() a, b := twoUsers(t, users) first, err := svc.Apply(ctx, a.ID, &ApplyInput{Email: "test02@spark.com", Reason: "hi", Remark: "work"}) if err != nil || first.Status != entity.RequestPending || first.RequestID == 0 { t.Fatalf("email apply %+v %v", first, err) } second, err := svc.Apply(ctx, a.ID, &ApplyInput{SparkID: b.SparkID, Reason: "again"}) if err != nil { t.Fatal(err) } if second.RequestID != first.RequestID { t.Fatalf("dedupe %d %d", first.RequestID, second.RequestID) } third, err := svc.Apply(ctx, a.ID, &ApplyInput{QRCode: b.QRCode, Reason: "qr"}) if err != nil || third.RequestID != first.RequestID { t.Fatalf("qr %+v %v", third, err) } list, err := svc.ListRequests(ctx, b.ID, 20) if err != nil || len(list.Items) != 1 || list.Items[0].Reason != "qr" || list.Items[0].From.UID != a.ID { t.Fatalf("inbox %+v %v", list, err) } } func TestAcceptBecomesFriendsAndGreeting(t *testing.T) { svc, users, g := testFriends(t) ctx := t.Context() a, b := twoUsers(t, users) applied, err := svc.Apply(ctx, a.ID, &ApplyInput{Email: "test02@spark.com", Remark: "from-a"}) if err != nil { t.Fatal(err) } got, err := svc.Accept(ctx, b.ID, AcceptInput{RequestID: applied.RequestID, Remark: "from-b"}) if err != nil || got.UID != a.ID { t.Fatalf("accept %+v %v", got, err) } if g.n != 1 { t.Fatalf("greeting %d", g.n) } if err := svc.CanSend(ctx, a.ID, b.ID); err != nil { t.Fatal(err) } view, err := svc.Get(ctx, a.ID, b.ID) if err != nil || view.Remark != "from-a" || view.Relation != entity.RelationFriend { t.Fatalf("a view %+v %v", view, err) } view, err = svc.Get(ctx, b.ID, a.ID) if err != nil || view.Remark != "from-b" { t.Fatalf("b view %+v %v", view, err) } if _, err := svc.Get(ctx, a.ID, a.ID); !errors.Is(err, errs.ErrNotFound) { t.Fatalf("self %v", err) } } func TestMutualApplyAutoAccept(t *testing.T) { svc, users, g := testFriends(t) ctx := t.Context() a, b := twoUsers(t, users) if _, err := svc.Apply(ctx, a.ID, &ApplyInput{UID: b.ID, Remark: "a-note"}); err != nil { t.Fatal(err) } out, err := svc.Apply(ctx, b.ID, &ApplyInput{Email: "test01@spark.com", Remark: "b-note"}) if err != nil || out.Status != entity.RequestAccepted { t.Fatalf("%+v %v", out, err) } if g.n != 1 { t.Fatalf("greeting %d", g.n) } if err := svc.CanSend(ctx, b.ID, a.ID); err != nil { t.Fatal(err) } } func TestNoUnidirectionalDeleteAndBlock(t *testing.T) { svc, users, _ := testFriends(t) ctx := t.Context() a, b := twoUsers(t, users) applied, _ := svc.Apply(ctx, a.ID, &ApplyInput{Email: "test02@spark.com"}) if _, err := svc.Accept(ctx, b.ID, AcceptInput{RequestID: applied.RequestID}); err != nil { t.Fatal(err) } if err := svc.Delete(ctx, a.ID, b.ID); err != nil { t.Fatal(err) } if err := svc.CanSend(ctx, b.ID, a.ID); !errors.Is(err, errs.ErrForbidden) { t.Fatalf("after delete %v", err) } applied, _ = svc.Apply(ctx, a.ID, &ApplyInput{Email: "test02@spark.com"}) if _, err := svc.Accept(ctx, b.ID, AcceptInput{RequestID: applied.RequestID}); err != nil { t.Fatal(err) } if err := svc.Block(ctx, a.ID, b.ID); err != nil { t.Fatal(err) } ok, err := svc.AreFriends(ctx, a.ID, b.ID) if err != nil || !ok { t.Fatalf("block keeps friendship %v %v", ok, err) } view, err := svc.Get(ctx, a.ID, b.ID) if err != nil || !view.Blocked || view.Relation != entity.RelationBlock { t.Fatalf("blocked view %+v %v", view, err) } if err := svc.CanSend(ctx, a.ID, b.ID); !errors.Is(err, errs.ErrForbidden) { t.Fatalf("blocked send %v", err) } if _, err := svc.Apply(ctx, a.ID, &ApplyInput{Email: "test02@spark.com"}); !errors.Is(err, errs.ErrForbidden) { t.Fatalf("apply blocked %v", err) } if _, err := svc.Apply(ctx, b.ID, &ApplyInput{Email: "test01@spark.com"}); !errors.Is(err, errs.ErrNotFound) { t.Fatalf("apply blocked-by %v", err) } } func TestPinOrdersList(t *testing.T) { svc, users, _ := testFriends(t) ctx := t.Context() a, b := twoUsers(t, users) c, err := users.AuthenticateEmail(ctx, "test03@spark.com", "1", &userentity.LoginDevice{DeviceID: 3, DeviceType: userentity.DeviceIOS}) if err != nil { t.Fatal(err) } for _, peer := range []int64{b.ID, c.ID} { applied, err := svc.Apply(ctx, a.ID, &ApplyInput{UID: peer}) if err != nil { t.Fatal(err) } if _, err := svc.Accept(ctx, peer, AcceptInput{RequestID: applied.RequestID}); err != nil { t.Fatal(err) } } if err := svc.Pin(ctx, a.ID, c.ID); err != nil { t.Fatal(err) } list, err := svc.List(ctx, a.ID, 20) if err != nil || len(list.Items) != 2 || list.Items[0].UID != c.ID || !list.Items[0].Pinned { t.Fatalf("pinned first %+v %v", list, err) } if err := svc.Unpin(ctx, a.ID, c.ID); err != nil { t.Fatal(err) } list, err = svc.List(ctx, a.ID, 20) if err != nil || list.Items[0].Pinned { t.Fatalf("unpinned %+v %v", list, err) } } func TestRemarkReportLookup(t *testing.T) { svc, users, _ := testFriends(t) ctx := t.Context() a, b := twoUsers(t, users) look, err := svc.Lookup(ctx, a.ID, LookupInput{Q: b.SparkID}) if err != nil || look.UID != b.ID || look.Relation != entity.RelationNone { t.Fatalf("lookup %+v %v", look, err) } applied, _ := svc.Apply(ctx, a.ID, &ApplyInput{SparkID: b.SparkID}) if _, err := svc.Accept(ctx, b.ID, AcceptInput{RequestID: applied.RequestID}); err != nil { t.Fatal(err) } if err := svc.SetRemark(ctx, a.ID, b.ID, RemarkInput{Remark: "boss"}); err != nil { t.Fatal(err) } view, err := svc.Get(ctx, a.ID, b.ID) if err != nil || view.Remark != "boss" { t.Fatalf("remark %+v %v", view, err) } if err := svc.Report(ctx, a.ID, b.ID, ReportInput{Reason: entity.ReportSpam, Detail: "ads"}); err != nil { t.Fatal(err) } if _, err := svc.Apply(ctx, a.ID, &ApplyInput{Email: "test02@spark.com"}); !errors.Is(err, errs.ErrDuplicate) { t.Fatalf("already %v", err) } } func TestLookupQueryClassifies(t *testing.T) { svc, users, _ := testFriends(t) ctx := t.Context() a, b := twoUsers(t, users) queries := []string{ "test02@spark.com", strconv.FormatInt(b.ID, 10), b.SparkID, b.QRCode, } for _, q := range queries { look, err := svc.Lookup(ctx, a.ID, LookupInput{Q: q}) if err != nil || look.UID != b.ID { t.Fatalf("q=%q %+v %v", q, look, err) } } if _, err := svc.Lookup(ctx, a.ID, LookupInput{Q: ""}); !errors.Is(err, errs.ErrInvalidArgument) { t.Fatalf("empty %v", err) } if _, err := svc.Lookup(ctx, a.ID, LookupInput{Q: "nobody_xx"}); !errors.Is(err, errs.ErrNotFound) { t.Fatalf("missing %v", err) } } func TestSystemGreeterWritesMessage(t *testing.T) { ids, err := id.New(1) if err != nil { t.Fatal(err) } msgs := msgmodel.NewMemoryRepo() chats := convlogic.NewP2PService(convmodel.NewMemoryP2P(), ids) g := NewGreeter(chats, msgs, ids, delivermodel.NewMemoryQueue()) if err := g.BecameFriends(t.Context(), 11, 22); err != nil { t.Fatal(err) } convID, err := chats.EnsureP2P(t.Context(), 11, 22) if err != nil || convID == 0 { t.Fatalf("conv %d %v", convID, err) } found := false for _, msg := range msgs.All() { if msg.ConversationID == convID && msg.Type == msgentity.TypeSystem { found = true payload, err := msg.Payload() if err != nil || payload.System == nil || payload.System.Kind != entity.SystemKindBecameFriends { t.Fatalf("payload %+v %v", payload, err) } } } if !found { t.Fatal("missing system message") } }