package auth import ( "context" "errors" "testing" "time" accountapp "telesrv/internal/app/account" "telesrv/internal/domain" "telesrv/internal/store/memory" ) func TestEmailSignupSendCodeRoutesFreshSignupToEmail(t *testing.T) { ctx := context.Background() users := memory.NewUserStore() authz := memory.NewAuthorizationStore() sender := &testMailSender{} svc := NewService(users, authz, memory.NewCodeStore(), nil, nil, "12345", WithLoginEmail(LoginEmailOptions{Sender: sender}), WithEmailSignup(true)) phone, ok := domain.EncodeEmailPhone("newuser@owpengram.local") if !ok { t.Fatalf("EncodeEmailPhone: ok=false") } hash, err := svc.SendCode(ctx, phone) if err != nil { t.Fatalf("SendCode: %v", err) } if sender.to != "newuser@owpengram.local" { t.Fatalf("sender.to = %q, want newuser@owpengram.local", sender.to) } delivery, found, err := svc.CodeDelivery(ctx, hash) if err != nil || !found { t.Fatalf("CodeDelivery found=%v err=%v", found, err) } if delivery.Kind != domain.AuthCodeDeliveryEmail { t.Fatalf("delivery.Kind = %v, want AuthCodeDeliveryEmail", delivery.Kind) } // The email-signup path must reuse the stock auth.signIn/SignInWithEmail // flow unchanged: a fresh (never-registered) 888 phone reports needSignUp. _, _, needSignUp, err := svc.SignInWithEmail(ctx, domain.Authorization{}, phone, hash, sender.code) if err != nil { t.Fatalf("SignInWithEmail: %v", err) } if !needSignUp { t.Fatalf("needSignUp = false, want true for a brand-new email-signup account") } // SignUp itself is called exactly like the plain-phone path (same phone // argument, no email-specific parameter anywhere), but the account it // creates gets a short, normal-looking "888" display number instead of // storing the long email-encoded wire value as its permanent phone; the // email->user association lives in SignupEmail instead. created, _, err := svc.SignUp(ctx, domain.Authorization{}, phone, hash, "New", "User") if err != nil { t.Fatalf("SignUp: %v", err) } if created.Phone == phone || !domain.ValidPhone(created.Phone) || domain.IsEmailSignupPhone(created.Phone) { t.Fatalf("created.Phone = %q, want a short all-digit 888 display number distinct from the wire value %q", created.Phone, phone) } if created.SignupEmail != "newuser@owpengram.local" { t.Fatalf("created.SignupEmail = %q, want newuser@owpengram.local", created.SignupEmail) } } // Regression test: TELESRV_LOGIN_EMAIL_REQUIRE_SETUP=true (a real deployment // combo, not just EMAIL_SIGNUP_ENABLE alone) used to permanently reject // SignUp for every email-signup account with ErrCodeInvalid, because SignUp's // "must have a verified/pending login email" gate only recognized the legacy // VerifiedEmail/PendingEmail fields, which the email-signup path never sets. func TestEmailSignupSignUpSucceedsWithLoginEmailRequireSetupAlsoOn(t *testing.T) { ctx := context.Background() users := memory.NewUserStore() authz := memory.NewAuthorizationStore() sender := &testMailSender{} svc := NewService(users, authz, memory.NewCodeStore(), nil, nil, "12345", WithLoginEmail(LoginEmailOptions{Sender: sender, RequireSetup: true, Enabled: true}), WithEmailSignup(true)) phone, ok := domain.EncodeEmailPhone("requiresetup@owpengram.local") if !ok { t.Fatalf("EncodeEmailPhone: ok=false") } hash, err := svc.SendCode(ctx, phone) if err != nil { t.Fatalf("SendCode: %v", err) } if _, _, needSignUp, err := svc.SignInWithEmail(ctx, domain.Authorization{}, phone, hash, sender.code); err != nil || !needSignUp { t.Fatalf("SignInWithEmail: needSignUp=%v err=%v", needSignUp, err) } created, _, err := svc.SignUp(ctx, domain.Authorization{}, phone, hash, "Needs", "Setup") if err != nil { t.Fatalf("SignUp: %v (this is the loop bug if it fails with ErrCodeInvalid)", err) } if created.Phone == phone || !domain.ValidPhone(created.Phone) || domain.IsEmailSignupPhone(created.Phone) { t.Fatalf("created.Phone = %q, want a short all-digit 888 display number distinct from the wire value %q", created.Phone, phone) } if created.SignupEmail != "requiresetup@owpengram.local" { t.Fatalf("created.SignupEmail = %q, want requiresetup@owpengram.local", created.SignupEmail) } } func TestEmailSignupSendCodeIgnoredWhenPhoneIsNotEncoded(t *testing.T) { ctx := context.Background() users := memory.NewUserStore() authz := memory.NewAuthorizationStore() sender := &testMailSender{} svc := NewService(users, authz, memory.NewCodeStore(), nil, nil, "12345", WithLoginEmail(LoginEmailOptions{Sender: sender}), WithEmailSignup(true)) hash, err := svc.SendCode(ctx, "+15550019999") if err != nil { t.Fatalf("SendCode: %v", err) } if sender.to != "" { t.Fatalf("sender.to = %q, want empty (real phone must fall through to the normal dev-code path)", sender.to) } delivery, found, err := svc.CodeDelivery(ctx, hash) if err != nil || !found { t.Fatalf("CodeDelivery found=%v err=%v", found, err) } if delivery.Kind == domain.AuthCodeDeliveryEmail { t.Fatalf("delivery.Kind = Email, want a non-email fallback for a real phone number") } } // This server has no real SMS delivery, so a real phone number is only // reachable via the client's explicit "log in by phone number" fallback // (see EmailSignupWidget). That account must get a login email bound // immediately: EmailSignupEnable alone must force the setup-required gate // even though neither LOGIN_EMAIL_ENABLE nor LOGIN_EMAIL_REQUIRE_SETUP is on. func TestEmailSignupForcesLoginEmailSetupForRealPhoneNumbers(t *testing.T) { ctx := context.Background() users := memory.NewUserStore() authz := memory.NewAuthorizationStore() sender := &testMailSender{} svc := NewService(users, authz, memory.NewCodeStore(), nil, nil, "12345", WithLoginEmail(LoginEmailOptions{Store: &testLoginEmailStore{emails: map[string]string{}}, Sender: sender}), WithEmailSignup(true)) hash, err := svc.SendCode(ctx, "+15550029999") if err != nil { t.Fatalf("SendCode: %v", err) } if sender.to != "" { t.Fatalf("sender.to = %q, want empty (setup-required never sends a code itself)", sender.to) } delivery, found, err := svc.CodeDelivery(ctx, hash) if err != nil || !found { t.Fatalf("CodeDelivery found=%v err=%v", found, err) } if delivery.Kind != domain.AuthCodeDeliveryEmailSetupRequired { t.Fatalf("delivery.Kind = %v, want AuthCodeDeliveryEmailSetupRequired", delivery.Kind) } // Signing up straight off this code without ever completing email setup // (the account.sendVerifyEmailCode/verifyEmail round trip) must be // rejected: the hash was never marked SignUpVerified. if _, _, err := svc.SignUp(ctx, domain.Authorization{}, "+15550029999", hash, "No", "Email"); !errors.Is(err, ErrCodeInvalid) { t.Fatalf("SignUp err = %v, want ErrCodeInvalid (email setup was never completed)", err) } } // An email-signup account's whole identity is the email it verified, so // Settings' "login email" (account_passwords.login_email, the older // recovery-email feature reused here for delivery) must reflect it right // after SignUp — otherwise a freshly created account looks like it has no // login email configured even though it plainly does. func TestEmailSignupSignUpPopulatesLoginEmail(t *testing.T) { ctx := context.Background() users := memory.NewUserStore() authz := memory.NewAuthorizationStore() codes := memory.NewCodeStore() passwords := memory.NewPasswordStore() sender := &testMailSender{} accountSvc := accountapp.NewService(passwords, accountapp.WithUsers(users), accountapp.WithLoginEmailVerification(codes, sender, time.Minute, 3, 6), ) svc := NewService(users, authz, codes, nil, nil, "12345", WithLoginEmail(LoginEmailOptions{Store: accountSvc, Sender: sender}), WithEmailSignup(true)) phone, ok := domain.EncodeEmailPhone("loginemail@owpengram.local") if !ok { t.Fatalf("EncodeEmailPhone: ok=false") } hash, err := svc.SendCode(ctx, phone) if err != nil { t.Fatalf("SendCode: %v", err) } if _, _, needSignUp, err := svc.SignInWithEmail(ctx, domain.Authorization{}, phone, hash, sender.code); err != nil || !needSignUp { t.Fatalf("SignInWithEmail: needSignUp=%v err=%v", needSignUp, err) } u, _, err := svc.SignUp(ctx, domain.Authorization{}, phone, hash, "Login", "Email") if err != nil { t.Fatalf("SignUp: %v", err) } if email, found, err := accountSvc.LoginEmail(ctx, u.ID); err != nil || !found || email != "loginemail@owpengram.local" { t.Fatalf("LoginEmail(%d) = %q found=%v err=%v, want loginemail@owpengram.local", u.ID, email, found, err) } if email, found, err := accountSvc.LoginEmailByPhone(ctx, u.Phone); err != nil || !found || email != "loginemail@owpengram.local" { t.Fatalf("LoginEmailByPhone(%q) = %q found=%v err=%v", u.Phone, email, found, err) } }