Initial open source release

This commit is contained in:
A 2026-06-04 01:37:39 +08:00
commit 74992e893f
377 changed files with 118084 additions and 0 deletions

View 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)
}
}