package logic import ( "context" "errors" "testing" smsentity "spark-server/internal/module/sms/entity" smsmodel "spark-server/internal/module/sms/model" "spark-server/internal/pkg/errs" "spark-server/internal/pkg/reqctx" ) type stubEmail struct { n int } func (s *stubEmail) SendOTP(context.Context, string, smsentity.Purpose, string, string) error { s.n++ return nil } type stubPhone struct { carrier smsentity.Carrier n int } func (s *stubPhone) Carrier() smsentity.Carrier { return s.carrier } func (s *stubPhone) SendOTP(context.Context, string, smsentity.Purpose, string) error { s.n++ return nil } type stubAccounts struct { email string } func (s stubAccounts) EmailByUserID(context.Context, int64) (string, error) { return s.email, nil } func TestSendUnifiedEmail(t *testing.T) { store := smsmodel.NewMemoryStore() mail := &stubEmail{} svc := NewService(store, mail, nil, nil) out, err := svc.Send(t.Context(), SendCodeInput{ Purpose: smsentity.PurposeLogin, Target: "ada@example.com", }) if err != nil || out == nil || mail.n != 1 { t.Fatalf("send %+v %v mail=%d", out, err, mail.n) } } func TestSendPhoneDispatchesByCarrier(t *testing.T) { store := smsmodel.NewMemoryStore() mobile := &stubPhone{carrier: smsentity.CarrierMobile} unicom := &stubPhone{carrier: smsentity.CarrierUnicom} svc := NewService(store, nil, []PhoneCarrierSender{mobile, unicom}, nil) _, err := svc.Send(t.Context(), SendCodeInput{ Purpose: smsentity.PurposeLogin, Target: "13812345678", }) if err != nil || mobile.n != 1 || unicom.n != 0 { t.Fatalf("mobile %v mobile=%d unicom=%d", err, mobile.n, unicom.n) } _, err = svc.Send(t.Context(), SendCodeInput{ Purpose: smsentity.PurposeLogin, Target: "13012345678", }) if err != nil || unicom.n != 1 { t.Fatalf("unicom %v unicom=%d", err, unicom.n) } } func TestGuardAccountDeletePurpose(t *testing.T) { store := smsmodel.NewMemoryStore() svc := NewService(store, &stubEmail{}, nil, stubAccounts{email: "me@example.com"}) ctx := reqctx.With(t.Context(), &reqctx.RequestContext{UserID: 9}) _, err := svc.Send(ctx, SendCodeInput{ Purpose: smsentity.PurposeAccountDelete, Target: "other@example.com", }) if !errors.Is(err, errs.ErrForbidden) { t.Fatalf("wrong email %v", err) } _, err = svc.Send(ctx, SendCodeInput{ Purpose: smsentity.PurposeAccountDelete, Target: "me@example.com", }) if err != nil { t.Fatal(err) } } func TestClearTargetRemovesCode(t *testing.T) { store := smsmodel.NewMemoryStore() svc := NewService(store, nil, nil, nil) ctx := t.Context() email := "ada@example.com" key := storeKey(smsentity.ChannelEmail, smsentity.PurposeLogin, email) if err := store.Put(ctx, key, "123456", 60); err != nil { t.Fatal(err) } svc.ClearTarget(ctx, smsentity.ChannelEmail, email) if err := svc.CheckAndConsume(ctx, smsentity.ChannelEmail, smsentity.PurposeLogin, email, "123456"); !errors.Is(err, errs.ErrVerifyCodeInvalid) { t.Fatalf("still there %v", err) } }