Initial open source release
This commit is contained in:
commit
74992e893f
377 changed files with 118084 additions and 0 deletions
457
internal/mtprotoedge/login_e2e_test.go
Normal file
457
internal/mtprotoedge/login_e2e_test.go
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
package mtprotoedge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
"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/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()
|
||||
if err := helpStore.UpsertAppConfig(context.Background(), domain.AppConfig{
|
||||
Client: "tdesktop",
|
||||
Hash: 4,
|
||||
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(), 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, RPC: 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: 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
|
||||
}
|
||||
if cfg, ok := appConfig.(*tg.HelpAppConfig); !ok || cfg.Hash != 4 {
|
||||
t.Fatalf("help.getAppConfig = %T %+v, want hash=4 config", appConfig, appConfig)
|
||||
}
|
||||
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(), 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, RPC: 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: 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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue