owpengram-server/internal/mtprotoedge/login_e2e_test.go

485 lines
16 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/iamxvbaba/td/clock"
"github.com/iamxvbaba/td/exchange"
"github.com/iamxvbaba/td/session"
"github.com/iamxvbaba/td/telegram"
"github.com/iamxvbaba/td/telegram/dcs"
"github.com/iamxvbaba/td/tg"
"github.com/iamxvbaba/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/langpack"
messageapp "telesrv/internal/app/messages"
"telesrv/internal/app/updates"
"telesrv/internal/app/users"
"telesrv/internal/domain"
"telesrv/internal/rpc"
"telesrv/internal/store/memory"
)
// TestLoginRegisterFlow 是登录注册闭环的端到端验证telegram.Client 连本地 server
// 依次 sendCode → signIn(需注册) → signUp → getUsers(self),验证注册后能用 self 查回自己。
func TestLoginRegisterFlow(t *testing.T) {
const (
dc = 2
phone = "+8613800138000"
wantPhone = "8613800138000"
code = "12345"
)
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()
helpStore := memory.NewHelpStore()
// seed hash 必须高于 help service 的代码默认 hash低于默认值的 store 行会被
// 视为陈旧 seed 残留而被默认 config 覆盖),否则断言拿到的是默认 config。
const seedAppConfigHash = 1_000_000
if err := helpStore.UpsertAppConfig(context.Background(), domain.AppConfig{
Client: "tdesktop",
Hash: seedAppConfigHash,
JSON: []byte(`{"chat_read_mark_expire_period":604800,"chat_read_mark_size_threshold":50,"pm_read_date_expire_period":604800,"quote_length_max":1024,"telegram_antispam_group_size_min":200,"telegram_antispam_user_id":"5434988373"}`),
}); err != nil {
t.Fatalf("seed app config: %v", err)
}
if err := helpStore.UpsertCountries(context.Background(), []domain.Country{
{ISO2: "US", DefaultName: "United States", CountryCodes: []domain.CountryCode{{CountryCode: "1", Prefixes: []string{"1"}}}},
}); err != nil {
t.Fatalf("seed countries: %v", err)
}
langPackStore := memory.NewLangPackStore()
if err := langPackStore.UpsertPack(context.Background(), domain.LangPack{
LangPack: "tdesktop",
LangCode: "en",
Version: 1,
Strings: []domain.LangPackString{{Key: "lng_language_name", Value: "English"}},
}); err != nil {
t.Fatalf("seed langpack: %v", err)
}
deps := rpc.Deps{
Auth: auth.NewService(userStore, authzStore, memory.NewCodeStore(), authKeyStore, memory.NewTempAuthKeyBindingStore(authKeyStore), code),
Account: account.NewService(memory.NewPasswordStore()),
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()),
LangPack: langpack.NewService(langPackStore),
}
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, LayerRPC: router})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
serveErr := make(chan error, 1)
go func() { serveErr <- srv.Serve(ctx, ln) }()
opts := 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{},
UpdateHandler: telegram.UpdateHandlerFunc(func(context.Context, tg.UpdatesClass) error { return nil }),
}
client := telegram.NewClient(1, "hash", opts)
if err := client.Run(ctx, func(ctx context.Context) error {
raw := tg.NewClient(client)
// 1) sendCode → phone_code_hash
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 {
t.Fatalf("sendCode result = %T, want *tg.AuthSentCode", sent)
}
hash := sentCode.PhoneCodeHash
// 2) signIn → 新用户应得 SignUpRequired
signInRes, err := raw.AuthSignIn(ctx, &tg.AuthSignInRequest{
PhoneNumber: phone,
PhoneCodeHash: hash,
PhoneCode: code,
})
if err != nil {
return err
}
if _, ok := signInRes.(*tg.AuthAuthorizationSignUpRequired); !ok {
t.Fatalf("signIn result = %T, want *tg.AuthAuthorizationSignUpRequired", signInRes)
}
// 3) signUp → 创建用户并返回授权
signUpRes, err := raw.AuthSignUp(ctx, &tg.AuthSignUpRequest{
PhoneNumber: phone,
PhoneCodeHash: hash,
FirstName: "Test",
LastName: "User",
})
if err != nil {
return err
}
authz, ok := signUpRes.(*tg.AuthAuthorization)
if !ok {
t.Fatalf("signUp result = %T, want *tg.AuthAuthorization", signUpRes)
}
newUser, ok := authz.User.(*tg.User)
if !ok {
t.Fatalf("signUp user = %T, want *tg.User", authz.User)
}
if !newUser.Self || newUser.FirstName != "Test" || newUser.Phone != wantPhone {
t.Fatalf("signUp user = %+v, want self FirstName=Test Phone=%s", newUser, wantPhone)
}
// 4) getUsers(self) → 注册后能查回自己
got, err := raw.UsersGetUsers(ctx, []tg.InputUserClass{&tg.InputUserSelf{}})
if err != nil {
return err
}
if len(got) != 1 {
t.Fatalf("getUsers returned %d users, want 1", len(got))
}
self, ok := got[0].(*tg.User)
if !ok {
t.Fatalf("getUsers[0] = %T, want *tg.User", got[0])
}
if self.ID != newUser.ID || self.FirstName != "Test" || self.Phone != wantPhone {
t.Fatalf("getUsers self = %+v, want id=%d FirstName=Test Phone=%s", self, newUser.ID, wantPhone)
}
// 5) 启动配置、账号安全和登录后的空账号 RPC 走业务服务并可编码。
appConfig, err := raw.HelpGetAppConfig(ctx, 0)
if err != nil {
return err
}
// 注意client.Run 回调里 t.Fatalf 只会杀当前 goroutine、测试主协程
// 会等到 ctx 超时——断言失败用 return fmt.Errorf 让 Run 立即返回。
cfg, ok := appConfig.(*tg.HelpAppConfig)
if !ok || cfg.Hash == 0 || cfg.Hash == seedAppConfigHash {
return fmt.Errorf("help.getAppConfig = %T %+v, want authenticated overlay hash distinct from seed=%d", appConfig, appConfig, seedAppConfigHash)
}
object, ok := cfg.Config.(*tg.JSONObject)
if !ok {
return fmt.Errorf("help.getAppConfig config = %T, want *tg.JSONObject", cfg.Config)
}
values := make(map[string]tg.JSONValueClass, len(object.Value))
for _, item := range object.Value {
values[item.Key] = item.Value
}
for _, key := range []string{"freeze_since_date", "freeze_until_date"} {
value, ok := values[key].(*tg.JSONNumber)
if !ok || value.Value != 0 {
return fmt.Errorf("help.getAppConfig %s = %T %+v, want zero clear value", key, values[key], values[key])
}
}
if value, ok := values["freeze_appeal_url"].(*tg.JSONString); !ok || value.Value != "" {
return fmt.Errorf("help.getAppConfig freeze_appeal_url = %T %+v, want empty clear value", values["freeze_appeal_url"], values["freeze_appeal_url"])
}
if value, ok := values["quote_length_max"].(*tg.JSONNumber); !ok || value.Value != 1024 {
return fmt.Errorf("help.getAppConfig lost seeded base config: quote_length_max=%T %+v", values["quote_length_max"], values["quote_length_max"])
}
countriesRes, err := raw.HelpGetCountriesList(ctx, &tg.HelpGetCountriesListRequest{LangCode: "en"})
if err != nil {
return err
}
if countries, ok := countriesRes.(*tg.HelpCountriesList); !ok || len(countries.Countries) != 1 {
t.Fatalf("help.getCountriesList = %T %+v, want 1 country", countriesRes, countriesRes)
}
password, err := raw.AccountGetPassword(ctx)
if err != nil {
return err
}
if password.HasPassword || len(password.SecureRandom) == 0 {
t.Fatalf("account.getPassword = %+v, want no password with secure random", password)
}
state, err := raw.UpdatesGetState(ctx)
if err != nil {
return err
}
if state.Date == 0 {
t.Fatal("updates.getState Date is zero")
}
diff, err := raw.UpdatesGetDifference(ctx, &tg.UpdatesGetDifferenceRequest{
Pts: state.Pts,
Date: state.Date,
Qts: state.Qts,
})
if err != nil {
return err
}
if _, ok := diff.(*tg.UpdatesDifferenceEmpty); !ok {
t.Fatalf("updates.getDifference = %T, want *tg.UpdatesDifferenceEmpty", diff)
}
contactsRes, err := raw.ContactsGetContacts(ctx, 0)
if err != nil {
return err
}
if contacts, ok := contactsRes.(*tg.ContactsContacts); !ok || len(contacts.Contacts) != 0 {
t.Fatalf("contacts.getContacts = %T %+v, want empty *tg.ContactsContacts", contactsRes, contactsRes)
}
dialogsRes, err := raw.MessagesGetDialogs(ctx, &tg.MessagesGetDialogsRequest{
OffsetPeer: &tg.InputPeerEmpty{},
Limit: 20,
})
if err != nil {
return err
}
if dialogs, ok := dialogsRes.(*tg.MessagesDialogs); !ok || len(dialogs.Dialogs) != 0 {
t.Fatalf("messages.getDialogs = %T %+v, want empty *tg.MessagesDialogs", dialogsRes, dialogsRes)
}
pinned, err := raw.MessagesGetPinnedDialogs(ctx, 0)
if err != nil {
return err
}
if len(pinned.Dialogs) != 0 || pinned.State.Date == 0 {
t.Fatalf("messages.getPinnedDialogs = %+v, want empty dialogs with state", pinned)
}
pack, err := raw.LangpackGetLangPack(ctx, &tg.LangpackGetLangPackRequest{
LangPack: "tdesktop",
LangCode: "en",
})
if err != nil {
return err
}
if pack.Version != 1 || len(pack.Strings) != 1 {
t.Fatalf("langpack.getLangPack = %+v, want version 1 with 1 string", pack)
}
strings, err := raw.LangpackGetStrings(ctx, &tg.LangpackGetStringsRequest{
LangPack: "tdesktop",
LangCode: "en",
Keys: []string{"lng_language_name"},
})
if err != nil {
return err
}
if len(strings) != 1 {
t.Fatalf("langpack.getStrings returned %d strings, want 1", len(strings))
}
return nil
}); err != nil {
t.Fatalf("login/register flow: %v", err)
}
cancel()
if err := <-serveErr; err != nil {
t.Errorf("serve: %v", err)
}
}
func TestPrivateMessageRoundTripFlow(t *testing.T) {
const (
dc = 2
code = "12345"
)
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()
helpStore := memory.NewHelpStore()
langPackStore := memory.NewLangPackStore()
dialogStore := memory.NewDialogStore()
messageStore := memory.NewMessageStore(dialogStore)
activeSessions := NewSessionManager(zaptest.NewLogger(t).Named("sessions"))
deps := rpc.Deps{
Auth: auth.NewService(userStore, authzStore, memory.NewCodeStore(), authKeyStore, memory.NewTempAuthKeyBindingStore(authKeyStore), code),
Account: account.NewService(memory.NewPasswordStore()),
Help: help.NewService(helpStore, helpStore),
Users: users.NewService(userStore),
Updates: updates.NewService(memory.NewUpdateStateStore(), memory.NewUpdateEventStore()),
Contacts: contacts.NewService(memory.NewContactStore()),
Dialogs: dialogs.NewService(dialogStore),
Messages: messageapp.NewService(messageStore, dialogStore),
LangPack: langpack.NewService(langPackStore),
Sessions: activeSessions,
}
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, LayerRPC: router, ActiveSessions: activeSessions})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
serveErr := make(chan error, 1)
go func() { serveErr <- srv.Serve(ctx, ln) }()
newClient := func(storage *session.StorageMemory) *telegram.Client {
opts := 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: storage,
UpdateHandler: telegram.UpdateHandlerFunc(func(context.Context, tg.UpdatesClass) error { return nil }),
}
return telegram.NewClient(1, "hash", opts)
}
storageA := &session.StorageMemory{}
storageB := &session.StorageMemory{}
messagesOf := func(history tg.MessagesMessagesClass) []tg.MessageClass {
t.Helper()
switch v := history.(type) {
case *tg.MessagesMessages:
return v.Messages
case *tg.MessagesMessagesSlice:
return v.Messages
default:
t.Fatalf("history = %T %+v, want messages", history, history)
return nil
}
}
signUp := func(storage *session.StorageMemory, phone, firstName string) tg.User {
t.Helper()
client := newClient(storage)
var out tg.User
if err := client.Run(ctx, func(ctx context.Context) error {
raw := tg.NewClient(client)
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
}
res, err := raw.AuthSignUp(ctx, &tg.AuthSignUpRequest{
PhoneNumber: phone,
PhoneCodeHash: hash,
FirstName: firstName,
})
if err != nil {
return err
}
authz := res.(*tg.AuthAuthorization)
u := authz.User.(*tg.User)
out = *u
return nil
}); err != nil {
t.Fatalf("signUp %s: %v", firstName, err)
}
return out
}
userA := signUp(storageA, "+15550001001", "Alice")
userB := signUp(storageB, "+15550001002", "Bob")
sendAndRead := func(storage *session.StorageMemory, to tg.User, body string, randomID int64) {
t.Helper()
client := newClient(storage)
if err := client.Run(ctx, func(ctx context.Context) error {
raw := tg.NewClient(client)
updates, err := raw.MessagesSendMessage(ctx, &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerUser{UserID: to.ID, AccessHash: to.AccessHash},
Message: body,
RandomID: randomID,
})
if err != nil {
return err
}
gotUpdates, ok := updates.(*tg.Updates)
if !ok || len(gotUpdates.Updates) < 2 {
t.Fatalf("send updates = %T %+v, want message id + new message", updates, updates)
}
history, err := raw.MessagesGetHistory(ctx, &tg.MessagesGetHistoryRequest{
Peer: &tg.InputPeerUser{UserID: to.ID, AccessHash: to.AccessHash},
Limit: 10,
})
if err != nil {
return err
}
msgs := messagesOf(history)
if len(msgs) == 0 {
t.Fatalf("history = %T %+v, want messages", history, history)
}
msg, ok := msgs[0].(*tg.Message)
if !ok || msg.Message != body || !msg.Out {
t.Fatalf("latest history message = %#v, want outgoing %q", msgs[0], body)
}
return nil
}); err != nil {
t.Fatalf("send %q: %v", body, err)
}
}
sendAndRead(storageA, userB, "hello bob", 1001)
clientB := newClient(storageB)
if err := clientB.Run(ctx, func(ctx context.Context) error {
raw := tg.NewClient(clientB)
history, err := raw.MessagesGetHistory(ctx, &tg.MessagesGetHistoryRequest{
Peer: &tg.InputPeerUser{UserID: userA.ID, AccessHash: userA.AccessHash},
Limit: 10,
})
if err != nil {
return err
}
msgs := messagesOf(history)
if len(msgs) == 0 {
t.Fatalf("bob history = %T %+v, want incoming message", history, history)
}
msg, ok := msgs[0].(*tg.Message)
if !ok || msg.Message != "hello bob" || msg.Out {
t.Fatalf("bob latest message = %#v, want incoming hello bob", msgs[0])
}
return nil
}); err != nil {
t.Fatalf("bob read incoming: %v", err)
}
sendAndRead(storageB, userA, "hi alice", 2001)
cancel()
if err := <-serveErr; err != nil {
t.Errorf("serve: %v", err)
}
}