owpengram-server/internal/mtprotoedge/login_email_e2e_test.go

236 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mtprotoedge
import (
"context"
"crypto/rand"
"crypto/rsa"
"fmt"
"net"
"testing"
"time"
"go.uber.org/zap/zaptest"
"github.com/gotd/log/logzap"
"github.com/gotd/td/clock"
"github.com/gotd/td/exchange"
"github.com/gotd/td/session"
"github.com/gotd/td/telegram"
"github.com/gotd/td/telegram/dcs"
"github.com/gotd/td/tg"
"github.com/gotd/td/transport"
"telesrv/internal/app/account"
"telesrv/internal/app/auth"
"telesrv/internal/app/contacts"
"telesrv/internal/app/dialogs"
"telesrv/internal/app/help"
"telesrv/internal/app/updates"
"telesrv/internal/app/users"
"telesrv/internal/rpc"
"telesrv/internal/store/memory"
)
// TestLoginEmailEndToEnd 端到端验证登录邮箱:设备 A 注册并设置登录邮箱loginChange
// 一个全新设备 B 调 sendCode 收到 sentCodeTypeEmailCode凭任意邮箱验证码经 signIn
// (email_verification) 完成登录。
func TestLoginEmailEndToEnd(t *testing.T) {
const (
dc = 2
phone = "+8613800138777"
wantPhone = "8613800138777"
code = "12345"
email = "owner@example.com"
wantMask = "o***r@example.com"
)
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("gen rsa: %v", err)
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
tcpAddr := ln.Addr().(*net.TCPAddr)
userStore := memory.NewUserStore()
authzStore := memory.NewAuthorizationStore()
authKeyStore := memory.NewAuthKeyStore()
passwordStore := memory.NewPasswordStore()
helpStore := memory.NewHelpStore()
deps := rpc.Deps{
Auth: auth.NewService(userStore, authzStore, memory.NewCodeStore(), authKeyStore, memory.NewTempAuthKeyBindingStore(), code, auth.WithPasswords(passwordStore)),
Account: account.NewService(passwordStore, account.WithUsers(userStore)),
Help: help.NewService(helpStore, helpStore),
Users: users.NewService(userStore),
Updates: updates.NewService(memory.NewUpdateStateStore(), memory.NewUpdateEventStore()),
Contacts: contacts.NewService(memory.NewContactStore()),
Dialogs: dialogs.NewService(memory.NewDialogStore()),
}
router := rpc.New(rpc.Config{DC: dc, IP: tcpAddr.IP.String(), Port: tcpAddr.Port}, deps, zaptest.NewLogger(t), clock.System)
srv := New(Options{Logger: zaptest.NewLogger(t), DC: dc, RSAKey: rsaKey, AuthKeys: authKeyStore, RPC: router})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
serveErr := make(chan error, 1)
go func() { serveErr <- srv.Serve(ctx, ln) }()
// 关停顺序cancel 后等 Serve 真正返回(含其末尾日志)再让测试结束,避免
// 服务端 goroutine 在测试完成后写 zaptest logger 触发 panic。
defer func() {
cancel()
<-serveErr
}()
newClient := func() *telegram.Client {
return telegram.NewClient(1, "hash", telegram.Options{
PublicKeys: []exchange.PublicKey{{RSA: &rsaKey.PublicKey}},
Resolver: dcs.Plain(dcs.PlainOptions{Protocol: transport.Intermediate}),
DCList: dcs.List{Options: []tg.DCOption{{ID: dc, IPAddress: tcpAddr.IP.String(), Port: tcpAddr.Port, Static: true}}},
Logger: logzap.New(zaptest.NewLogger(t).Named("client")),
SessionStorage: &session.StorageMemory{}, // 每个 client 独立 session = 独立 auth key = 独立"设备"
UpdateHandler: telegram.UpdateHandlerFunc(func(context.Context, tg.UpdatesClass) error { return nil }),
})
}
// 设备 A注册并设置登录邮箱。client.Run 回调里断言失败一律 return error回调跑在
// 独立 goroutinet.Fatalf 只会杀该 goroutine 并让测试空等到超时)。
deviceA := newClient()
if err := deviceA.Run(ctx, func(ctx context.Context) error {
raw := tg.NewClient(deviceA)
sent, err := raw.AuthSendCode(ctx, &tg.AuthSendCodeRequest{PhoneNumber: phone, APIID: 1, APIHash: "hash", Settings: tg.CodeSettings{}})
if err != nil {
return err
}
hash := sent.(*tg.AuthSentCode).PhoneCodeHash
if _, err := raw.AuthSignIn(ctx, &tg.AuthSignInRequest{PhoneNumber: phone, PhoneCodeHash: hash, PhoneCode: code}); err != nil {
return err
}
if _, err := raw.AuthSignUp(ctx, &tg.AuthSignUpRequest{PhoneNumber: phone, PhoneCodeHash: hash, FirstName: "Owner"}); err != nil {
return err
}
// 设置登录邮箱loginChange已登录
sentEmail, err := raw.AccountSendVerifyEmailCode(ctx, &tg.AccountSendVerifyEmailCodeRequest{
Purpose: &tg.EmailVerifyPurposeLoginChange{},
Email: email,
})
if err != nil {
return err
}
if sentEmail.EmailPattern != wantMask {
return fmt.Errorf("sentEmailCode pattern = %q, want %q", sentEmail.EmailPattern, wantMask)
}
verified, err := raw.AccountVerifyEmail(ctx, &tg.AccountVerifyEmailRequest{
Purpose: &tg.EmailVerifyPurposeLoginChange{},
Verification: &tg.EmailVerificationCode{Code: "whatever"},
})
if err != nil {
return err
}
ev, ok := verified.(*tg.AccountEmailVerified)
if !ok {
return fmt.Errorf("verifyEmail result = %T, want *tg.AccountEmailVerified", verified)
}
if ev.Email != email {
return fmt.Errorf("verifyEmail email = %q, want %q", ev.Email, email)
}
// getPassword 下发登录邮箱掩码。
pwd, err := raw.AccountGetPassword(ctx)
if err != nil {
return err
}
gotMask, ok := pwd.GetLoginEmailPattern()
if !ok || gotMask != wantMask {
return fmt.Errorf("getPassword login_email_pattern = %q ok=%v, want %q", gotMask, ok, wantMask)
}
return nil
}); err != nil {
t.Fatalf("device A: %v", err)
}
// 设备 B全新 auth keysendCode 应改投邮箱,凭任意邮箱码登录。
deviceB := newClient()
if err := deviceB.Run(ctx, func(ctx context.Context) error {
raw := tg.NewClient(deviceB)
sent, err := raw.AuthSendCode(ctx, &tg.AuthSendCodeRequest{PhoneNumber: phone, APIID: 1, APIHash: "hash", Settings: tg.CodeSettings{}})
if err != nil {
return err
}
sentCode, ok := sent.(*tg.AuthSentCode)
if !ok {
return fmt.Errorf("sendCode result = %T, want *tg.AuthSentCode", sent)
}
emailType, ok := sentCode.Type.(*tg.AuthSentCodeTypeEmailCode)
if !ok {
return fmt.Errorf("sendCode type = %T, want *tg.AuthSentCodeTypeEmailCode (login email should switch delivery)", sentCode.Type)
}
if emailType.EmailPattern != wantMask {
return fmt.Errorf("sentCodeTypeEmailCode pattern = %q, want %q", emailType.EmailPattern, wantMask)
}
signInRes, err := raw.AuthSignIn(ctx, &tg.AuthSignInRequest{
PhoneNumber: phone,
PhoneCodeHash: sentCode.PhoneCodeHash,
EmailVerification: &tg.EmailVerificationCode{Code: "any-email-code"},
})
if err != nil {
return err
}
authz, ok := signInRes.(*tg.AuthAuthorization)
if !ok {
return fmt.Errorf("signIn result = %T, want *tg.AuthAuthorization", signInRes)
}
self, ok := authz.User.(*tg.User)
if !ok || !self.Self || self.Phone != wantPhone {
return fmt.Errorf("signIn user = %+v, want self phone=%s", authz.User, wantPhone)
}
return nil
}); err != nil {
t.Fatalf("device B: %v", err)
}
// 设备 C无法访问登录邮箱 → resetLoginEmail 清除登录邮箱、改回手机验证码登录。
deviceC := newClient()
if err := deviceC.Run(ctx, func(ctx context.Context) error {
raw := tg.NewClient(deviceC)
sent, err := raw.AuthSendCode(ctx, &tg.AuthSendCodeRequest{PhoneNumber: phone, APIID: 1, APIHash: "hash", Settings: tg.CodeSettings{}})
if err != nil {
return err
}
sentCode := sent.(*tg.AuthSentCode)
if _, ok := sentCode.Type.(*tg.AuthSentCodeTypeEmailCode); !ok {
return fmt.Errorf("pre-reset sendCode type = %T, want email code", sentCode.Type)
}
// 重置登录邮箱:返回一个新的手机验证码 sentCodesentCodeTypeApp
resetRes, err := raw.AuthResetLoginEmail(ctx, &tg.AuthResetLoginEmailRequest{PhoneNumber: phone, PhoneCodeHash: sentCode.PhoneCodeHash})
if err != nil {
return err
}
resetSent, ok := resetRes.(*tg.AuthSentCode)
if !ok {
return fmt.Errorf("resetLoginEmail result = %T, want *tg.AuthSentCode", resetRes)
}
if _, ok := resetSent.Type.(*tg.AuthSentCodeTypeApp); !ok {
return fmt.Errorf("resetLoginEmail sentCode type = %T, want *tg.AuthSentCodeTypeApp (back to phone)", resetSent.Type)
}
// 用手机验证码完成登录。
signInRes, err := raw.AuthSignIn(ctx, &tg.AuthSignInRequest{PhoneNumber: phone, PhoneCodeHash: resetSent.PhoneCodeHash, PhoneCode: code})
if err != nil {
return err
}
if _, ok := signInRes.(*tg.AuthAuthorization); !ok {
return fmt.Errorf("post-reset signIn result = %T, want *tg.AuthAuthorization", signInRes)
}
return nil
}); err != nil {
t.Fatalf("device C: %v", err)
}
}