owpengram-server/internal/app/auth/login_email_config_test.go
A 1292540350 feat: sync langpack and auth updates
Sync telesrv commits 49f9bab and 04d9563 into the public mirror. Exclude private docs and runtime key material per sync rules.
2026-07-18 00:07:41 +08:00

263 lines
8.5 KiB
Go

package auth
import (
"context"
"errors"
"testing"
"telesrv/internal/domain"
"telesrv/internal/otpdelivery"
"telesrv/internal/store/memory"
)
type testLoginEmailStore struct {
emails map[string]string
}
func (s *testLoginEmailStore) LoginEmailByPhone(_ context.Context, phone string) (string, bool, error) {
email, ok := s.emails[domain.NormalizePhone(phone)]
return email, ok, nil
}
func (s *testLoginEmailStore) SetLoginEmail(_ context.Context, _ int64, _ string) error {
return nil
}
type testMailSender struct {
to string
code string
}
func (s *testMailSender) Deliver(_ context.Context, req otpdelivery.Request) (otpdelivery.Result, error) {
s.to = req.Recipient
s.code = req.Code
return otpdelivery.Result{}, nil
}
func TestConfiguredEmailLoginSharesAttemptsAcrossOfficialCodeCarriers(t *testing.T) {
ctx := context.Background()
users := memory.NewUserStore()
authz := memory.NewAuthorizationStore()
if _, err := users.Create(ctx, domain.User{Phone: "15550009101", FirstName: "Email"}); err != nil {
t.Fatalf("create user: %v", err)
}
emails := &testLoginEmailStore{emails: map[string]string{"15550009101": "alice@example.test"}}
sender := &testMailSender{}
appDelivery := &captureLoginCodeDelivery{}
svc := NewService(users, authz, memory.NewCodeStore(), nil, nil, "12345",
WithLoginCodeDelivery(appDelivery),
WithLoginEmail(LoginEmailOptions{
Enabled: true,
CodeLength: 6,
Store: emails,
Sender: sender,
}),
WithCodeMaxAttempts(2))
hash, err := svc.SendCode(ctx, "+1 555 000 9101")
if err != nil {
t.Fatalf("SendCode: %v", err)
}
if sender.to != "alice@example.test" || len(sender.code) != 6 {
t.Fatalf("sent email to/code = %q/%q, want alice@example.test/6 digits", sender.to, sender.code)
}
if len(appDelivery.requests) != 1 || appDelivery.requests[0].PhoneCodeHash != hash || appDelivery.requests[0].Code != sender.code {
t.Fatalf("App-code delivery=%+v, want same email code/hash", appDelivery.requests)
}
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 || delivery.EmailPattern != "a***e@example.test" || delivery.Length != 6 {
t.Fatalf("delivery = %+v, want email masked length 6", delivery)
}
bad1 := wrongCode(sender.code, '0')
bad2 := wrongCode(sender.code, '1')
if bad2 == bad1 {
bad2 = wrongCode(sender.code, '2')
}
if _, _, _, err := svc.SignIn(ctx, domain.Authorization{}, "+15550009101", hash, bad1); !errors.Is(err, ErrCodeInvalid) {
t.Fatalf("first bad WebK SignIn err = %v, want ErrCodeInvalid", err)
}
if _, _, _, err := svc.SignInWithEmail(ctx, domain.Authorization{}, "+15550009101", hash, bad2); !errors.Is(err, ErrCodeInvalid) {
t.Fatalf("second bad native SignInWithEmail err = %v, want ErrCodeInvalid", err)
}
if _, _, _, err := svc.SignIn(ctx, domain.Authorization{}, "+15550009101", hash, sender.code); !errors.Is(err, ErrCodeExpired) {
t.Fatalf("WebK SignIn after shared max attempts err = %v, want ErrCodeExpired", err)
}
}
func wrongCode(code string, digit byte) string {
if code == "" {
return string(digit)
}
out := make([]byte, len(code))
for i := range out {
out[i] = digit
}
if string(out) != code {
return string(out)
}
for i := range out {
out[i] = '9'
}
return string(out)
}
func TestConfiguredEmailLoginAcceptsOfficialCodeCarriers(t *testing.T) {
tests := []struct {
name string
phone string
email string
webK bool
}{
{name: "webk_phone_code", phone: "15550009102", email: "webk@example.test", webK: true},
{name: "native_email_verification", phone: "15550009103", email: "native@example.test"},
}
for i, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
users := memory.NewUserStore()
authz := memory.NewAuthorizationStore()
u, err := users.Create(ctx, domain.User{Phone: tc.phone, FirstName: "Email"})
if err != nil {
t.Fatalf("create user: %v", err)
}
emails := &testLoginEmailStore{emails: map[string]string{tc.phone: tc.email}}
sender := &testMailSender{}
appDelivery := &captureLoginCodeDelivery{}
var key [8]byte
key[0] = byte(0x91 + i)
svc := NewService(users, authz, memory.NewCodeStore(), nil, nil, "12345",
WithLoginCodeDelivery(appDelivery),
WithLoginEmail(LoginEmailOptions{
Enabled: true,
CodeLength: 6,
Store: emails,
Sender: sender,
}))
hash, err := svc.SendCode(ctx, tc.phone)
if err != nil {
t.Fatalf("SendCode: %v", err)
}
if len(appDelivery.requests) != 1 || appDelivery.requests[0].Code != sender.code {
t.Fatalf("App-code delivery=%+v, want same email code", appDelivery.requests)
}
var got domain.User
var needSignUp bool
if tc.webK {
if _, _, _, err := svc.SignIn(ctx, domain.Authorization{AuthKeyID: key}, tc.phone, hash, "12345"); !errors.Is(err, ErrCodeInvalid) {
t.Fatalf("WebK development code err=%v, want ErrCodeInvalid for random email channel", err)
}
got, _, needSignUp, err = svc.SignIn(ctx, domain.Authorization{AuthKeyID: key}, tc.phone, hash, sender.code)
} else {
got, _, needSignUp, err = svc.SignInWithEmail(ctx, domain.Authorization{AuthKeyID: key}, tc.phone, hash, sender.code)
}
if err != nil {
t.Fatalf("sign in: %v", err)
}
if needSignUp || got.ID != u.ID {
t.Fatalf("sign in got user=%d needSignUp=%v, want %d/false", got.ID, needSignUp, u.ID)
}
})
}
}
func TestConfiguredEmailLoginViaWebKStillHonorsTwoFactor(t *testing.T) {
ctx := context.Background()
users := memory.NewUserStore()
authz := memory.NewAuthorizationStore()
passwords := memory.NewPasswordStore()
u, err := users.Create(ctx, domain.User{Phone: "15550009104", FirstName: "Email"})
if err != nil {
t.Fatalf("create user: %v", err)
}
if err := passwords.Save(ctx, u.ID, domain.PasswordSettings{HasPassword: true}); err != nil {
t.Fatalf("save password settings: %v", err)
}
sender := &testMailSender{}
svc := NewService(users, authz, memory.NewCodeStore(), nil, nil, "12345",
WithPasswords(passwords),
WithLoginCodeDelivery(&captureLoginCodeDelivery{}),
WithLoginEmail(LoginEmailOptions{
Enabled: true,
CodeLength: 6,
Store: &testLoginEmailStore{emails: map[string]string{u.Phone: "2fa@example.test"}},
Sender: sender,
}))
var key [8]byte
key[0] = 0x94
hash, err := svc.SendCode(ctx, u.Phone)
if err != nil {
t.Fatalf("SendCode: %v", err)
}
got, _, _, err := svc.SignIn(ctx, domain.Authorization{AuthKeyID: key}, u.Phone, hash, sender.code)
if !errors.Is(err, domain.ErrSessionPasswordNeeded) {
t.Fatalf("WebK email SignIn err=%v, want ErrSessionPasswordNeeded", err)
}
if got.ID != u.ID {
t.Fatalf("WebK email SignIn user=%d, want pending 2FA user %d", got.ID, u.ID)
}
if bound, found, err := svc.UserID(ctx, key); err != nil || found || bound != 0 {
t.Fatalf("UserID after WebK email SignIn with 2FA=%d found=%v err=%v, want not-found", bound, found, err)
}
}
func TestConfiguredEmailLoginHasSingleConsumerAcrossOfficialCodeCarriers(t *testing.T) {
ctx := context.Background()
users := memory.NewUserStore()
u, err := users.Create(ctx, domain.User{Phone: "15550009105", FirstName: "Email"})
if err != nil {
t.Fatalf("create user: %v", err)
}
sender := &testMailSender{}
svc := NewService(users, memory.NewAuthorizationStore(), memory.NewCodeStore(), nil, nil, "12345",
WithLoginCodeDelivery(&captureLoginCodeDelivery{}),
WithLoginEmail(LoginEmailOptions{
Enabled: true,
CodeLength: 6,
Store: &testLoginEmailStore{emails: map[string]string{u.Phone: "race@example.test"}},
Sender: sender,
}))
hash, err := svc.SendCode(ctx, u.Phone)
if err != nil {
t.Fatalf("SendCode: %v", err)
}
start := make(chan struct{})
results := make(chan error, 2)
var webKKey, nativeKey [8]byte
webKKey[0] = 0x95
nativeKey[0] = 0x96
go func() {
<-start
_, _, _, err := svc.SignIn(ctx, domain.Authorization{AuthKeyID: webKKey}, u.Phone, hash, sender.code)
results <- err
}()
go func() {
<-start
_, _, _, err := svc.SignInWithEmail(ctx, domain.Authorization{AuthKeyID: nativeKey}, u.Phone, hash, sender.code)
results <- err
}()
close(start)
accepted, expired := 0, 0
for range 2 {
err := <-results
switch {
case err == nil:
accepted++
case errors.Is(err, ErrCodeExpired):
expired++
default:
t.Fatalf("concurrent sign in err=%v, want nil or ErrCodeExpired", err)
}
}
if accepted != 1 || expired != 1 {
t.Fatalf("concurrent results accepted=%d expired=%d, want 1/1", accepted, expired)
}
}