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 立即返回。 if cfg, ok := appConfig.(*tg.HelpAppConfig); !ok || cfg.Hash != seedAppConfigHash { return fmt.Errorf("help.getAppConfig = %T %+v, want seeded hash=%d config", appConfig, appConfig, seedAppConfigHash) } 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) } }