feat: sync public links and phone change updates
This commit is contained in:
parent
41c7f1d018
commit
da04c0fa6a
53 changed files with 3029 additions and 111 deletions
|
|
@ -19,6 +19,8 @@ func (r *Router) registerAccount(d *tg.ServerDispatcher) {
|
|||
d.OnAccountUnregisterDevice(func(ctx context.Context, req *tg.AccountUnregisterDeviceRequest) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
d.OnAccountSendChangePhoneCode(r.onAccountSendChangePhoneCode)
|
||||
d.OnAccountChangePhone(r.onAccountChangePhone)
|
||||
d.OnAccountCheckUsername(r.onAccountCheckUsername)
|
||||
d.OnAccountUpdateProfile(r.onAccountUpdateProfile)
|
||||
d.OnAccountUpdateUsername(r.onAccountUpdateUsername)
|
||||
|
|
|
|||
89
internal/rpc/account_phone.go
Normal file
89
internal/rpc/account_phone.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
type phoneChangeEventConfirmer interface {
|
||||
ConfirmEvent(ctx context.Context, authKeyID [8]byte, userID int64, event domain.UpdateEvent) error
|
||||
}
|
||||
|
||||
type phoneChangeReliableDispatchReporter interface {
|
||||
PhoneChangeUsesReliableDispatch() bool
|
||||
}
|
||||
|
||||
func (r *Router) onAccountSendChangePhoneCode(ctx context.Context, req *tg.AccountSendChangePhoneCodeRequest) (tg.AuthSentCodeClass, error) {
|
||||
userID, found, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !found || r.deps.Account == nil {
|
||||
return nil, authKeyUnregisteredErr()
|
||||
}
|
||||
authKeyID, ok := AuthKeyIDFrom(ctx)
|
||||
if !ok || authKeyID == ([8]byte{}) {
|
||||
return nil, authKeyUnregisteredErr()
|
||||
}
|
||||
sessionID, _ := SessionIDFrom(ctx)
|
||||
hash, delivery, err := r.deps.Account.SendChangePhoneCode(ctx, userID, authKeyID, sessionID, req.PhoneNumber)
|
||||
if err != nil {
|
||||
return nil, phoneChangeErr(err)
|
||||
}
|
||||
return tgSMSSentCode(hash, delivery.Length), nil
|
||||
}
|
||||
|
||||
func (r *Router) onAccountChangePhone(ctx context.Context, req *tg.AccountChangePhoneRequest) (tg.UserClass, error) {
|
||||
userID, found, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !found || r.deps.Account == nil {
|
||||
return nil, authKeyUnregisteredErr()
|
||||
}
|
||||
authKeyID, ok := AuthKeyIDFrom(ctx)
|
||||
if !ok || authKeyID == ([8]byte{}) {
|
||||
return nil, authKeyUnregisteredErr()
|
||||
}
|
||||
sessionID, _ := SessionIDFrom(ctx)
|
||||
result, err := r.deps.Account.ChangePhone(
|
||||
ctx,
|
||||
userID,
|
||||
authKeyID,
|
||||
sessionID,
|
||||
req.PhoneNumber,
|
||||
req.PhoneCodeHash,
|
||||
req.PhoneCode,
|
||||
int(r.clock.Now().Unix()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, phoneChangeErr(err)
|
||||
}
|
||||
if result.User.ID == 0 {
|
||||
return nil, internalErr()
|
||||
}
|
||||
r.invalidateRPCProjectionForUser(result.User.ID)
|
||||
if result.Event.Pts > 0 {
|
||||
if confirmer, ok := r.deps.Updates.(phoneChangeEventConfirmer); ok {
|
||||
if err := confirmer.ConfirmEvent(ctx, authKeyID, userID, result.Event); err != nil {
|
||||
// user/event/outbox 已原子提交,不能把已成功改号伪装成失败;当前
|
||||
// session 仍会收到 pts 簿记,设备水位存储可由后续 getDifference 自愈。
|
||||
r.log.Warn("confirm phone change event", zap.Int64("user_id", userID), zap.Int("pts", result.Event.Pts), zap.Error(err))
|
||||
}
|
||||
}
|
||||
reliable := false
|
||||
if reporter, ok := r.deps.Account.(phoneChangeReliableDispatchReporter); ok {
|
||||
reliable = reporter.PhoneChangeUsesReliableDispatch()
|
||||
}
|
||||
if !reliable {
|
||||
r.pushUserUpdates(ctx, userID, tgUpdateForOutboxEvent(result.Event))
|
||||
}
|
||||
r.bookkeepAuxPtsForCurrentSession(ctx, result.Event)
|
||||
}
|
||||
return r.tgSelfUser(result.User), nil
|
||||
}
|
||||
128
internal/rpc/account_phone_rpc_test.go
Normal file
128
internal/rpc/account_phone_rpc_test.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/clock"
|
||||
"github.com/gotd/td/tg"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
appaccount "telesrv/internal/app/account"
|
||||
appupdates "telesrv/internal/app/updates"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
func TestAccountChangePhoneRPCReturnsSelfPushesOthersAndReplaysDifference(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := memory.NewUserStore()
|
||||
auths := memory.NewAuthorizationStore()
|
||||
codes := memory.NewCodeStore()
|
||||
events := memory.NewUpdateEventStore()
|
||||
user, err := users.Create(ctx, domain.User{AccessHash: 401, Phone: "15550013001", FirstName: "Alice"})
|
||||
if err != nil {
|
||||
t.Fatalf("create user: %v", err)
|
||||
}
|
||||
authKeyID := [8]byte{4, 3, 2, 1}
|
||||
if err := auths.Bind(ctx, domain.Authorization{AuthKeyID: authKeyID, UserID: user.ID, CreatedAt: time.Now().Add(-48 * time.Hour)}); err != nil {
|
||||
t.Fatalf("bind auth: %v", err)
|
||||
}
|
||||
accountSvc := appaccount.NewService(
|
||||
memory.NewPasswordStore(),
|
||||
appaccount.WithUsers(users),
|
||||
appaccount.WithPhoneChange(memory.NewPhoneChangeStore(users, events), auths, codes, nil, "12345", time.Minute, 5),
|
||||
)
|
||||
sessions := &captureSessions{}
|
||||
r := New(Config{}, Deps{Account: accountSvc, Sessions: sessions}, zaptest.NewLogger(t), clock.System)
|
||||
reqCtx := WithSessionID(WithAuthKeyID(WithUserID(ctx, user.ID), authKeyID), 77)
|
||||
|
||||
sentClass, err := r.onAccountSendChangePhoneCode(reqCtx, &tg.AccountSendChangePhoneCodeRequest{PhoneNumber: "+1 555 001 3002"})
|
||||
if err != nil {
|
||||
t.Fatalf("send change phone code: %v", err)
|
||||
}
|
||||
sent, ok := sentClass.(*tg.AuthSentCode)
|
||||
if !ok {
|
||||
t.Fatalf("sent code = %T", sentClass)
|
||||
}
|
||||
if _, ok := sent.Type.(*tg.AuthSentCodeTypeSMS); !ok || sent.PhoneCodeHash == "" {
|
||||
t.Fatalf("sent code type/hash = %T/%q", sent.Type, sent.PhoneCodeHash)
|
||||
}
|
||||
|
||||
userClass, err := r.onAccountChangePhone(reqCtx, &tg.AccountChangePhoneRequest{
|
||||
PhoneNumber: "15550013002",
|
||||
PhoneCodeHash: sent.PhoneCodeHash,
|
||||
PhoneCode: "12345",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("change phone: %v", err)
|
||||
}
|
||||
self, ok := userClass.(*tg.User)
|
||||
if !ok || self.ID != user.ID || self.Phone != "15550013002" {
|
||||
t.Fatalf("returned self = %T %+v", userClass, userClass)
|
||||
}
|
||||
|
||||
otherPush, ok := sessions.lastUserPush().(*tg.Updates)
|
||||
if !ok || len(otherPush.Updates) != 2 {
|
||||
t.Fatalf("other-session push = %T %+v", sessions.lastUserPush(), sessions.lastUserPush())
|
||||
}
|
||||
phoneUpdate, ok := otherPush.Updates[0].(*tg.UpdateUserPhone)
|
||||
if !ok || phoneUpdate.UserID != user.ID || phoneUpdate.Phone != "15550013002" {
|
||||
t.Fatalf("phone update = %T %+v", otherPush.Updates[0], otherPush.Updates[0])
|
||||
}
|
||||
if _, ok := otherPush.Updates[1].(*tg.UpdateDeleteMessages); !ok {
|
||||
t.Fatalf("pts bookkeeping = %T", otherPush.Updates[1])
|
||||
}
|
||||
currentPush, ok := sessions.snapshot().message.(*tg.Updates)
|
||||
if !ok || len(currentPush.Updates) != 1 {
|
||||
t.Fatalf("current-session bookkeeping = %T %+v", sessions.snapshot().message, sessions.snapshot().message)
|
||||
}
|
||||
if _, ok := currentPush.Updates[0].(*tg.UpdateDeleteMessages); !ok {
|
||||
t.Fatalf("current bookkeeping update = %T", currentPush.Updates[0])
|
||||
}
|
||||
|
||||
updateSvc := appupdates.NewService(memory.NewUpdateStateStore(), events)
|
||||
diff, err := updateSvc.GetDifference(ctx, [8]byte{9}, user.ID, domain.UpdateState{Pts: 0})
|
||||
if err != nil {
|
||||
t.Fatalf("get difference: %v", err)
|
||||
}
|
||||
tgDiff, ok := tgUpdatesDifference(user.ID, diff).(*tg.UpdatesDifference)
|
||||
if !ok || len(tgDiff.OtherUpdates) != 1 {
|
||||
t.Fatalf("difference = %T %+v", tgUpdatesDifference(user.ID, diff), tgUpdatesDifference(user.ID, diff))
|
||||
}
|
||||
replayed, ok := tgDiff.OtherUpdates[0].(*tg.UpdateUserPhone)
|
||||
if !ok || replayed.UserID != user.ID || replayed.Phone != "15550013002" {
|
||||
t.Fatalf("replayed update = %T %+v", tgDiff.OtherUpdates[0], tgDiff.OtherUpdates[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountChangePhoneRPCMapsCodeAndOccupiedErrors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := memory.NewUserStore()
|
||||
auths := memory.NewAuthorizationStore()
|
||||
codes := memory.NewCodeStore()
|
||||
events := memory.NewUpdateEventStore()
|
||||
user, _ := users.Create(ctx, domain.User{AccessHash: 411, Phone: "15550013101", FirstName: "Alice"})
|
||||
occupied, _ := users.Create(ctx, domain.User{AccessHash: 412, Phone: "15550013102", FirstName: "Bob"})
|
||||
authKeyID := [8]byte{5, 4, 3, 2}
|
||||
if err := auths.Bind(ctx, domain.Authorization{AuthKeyID: authKeyID, UserID: user.ID}); err != nil {
|
||||
t.Fatalf("bind auth: %v", err)
|
||||
}
|
||||
accountSvc := appaccount.NewService(memory.NewPasswordStore(),
|
||||
appaccount.WithUsers(users),
|
||||
appaccount.WithPhoneChange(memory.NewPhoneChangeStore(users, events), auths, codes, nil, "12345", time.Minute, 5))
|
||||
r := New(Config{}, Deps{Account: accountSvc}, zaptest.NewLogger(t), clock.System)
|
||||
reqCtx := WithSessionID(WithAuthKeyID(WithUserID(ctx, user.ID), authKeyID), 88)
|
||||
|
||||
if _, err := r.onAccountSendChangePhoneCode(reqCtx, &tg.AccountSendChangePhoneCodeRequest{PhoneNumber: occupied.Phone}); err == nil {
|
||||
t.Fatal("occupied phone unexpectedly accepted")
|
||||
} else {
|
||||
assertPhoneRPCErr(t, err, "PHONE_NUMBER_OCCUPIED")
|
||||
}
|
||||
if _, err := r.onAccountChangePhone(reqCtx, &tg.AccountChangePhoneRequest{PhoneNumber: "15550013103"}); err == nil {
|
||||
t.Fatal("empty code unexpectedly accepted")
|
||||
} else {
|
||||
assertPhoneRPCErr(t, err, "PHONE_CODE_EMPTY")
|
||||
}
|
||||
}
|
||||
|
|
@ -267,6 +267,16 @@ func tgSentCodeWithLength(hash string, length int) tg.AuthSentCodeClass {
|
|||
}
|
||||
}
|
||||
|
||||
func tgSMSSentCode(hash string, length int) tg.AuthSentCodeClass {
|
||||
if length <= 0 {
|
||||
length = devCodeLength
|
||||
}
|
||||
return &tg.AuthSentCode{
|
||||
Type: &tg.AuthSentCodeTypeSMS{Length: length},
|
||||
PhoneCodeHash: hash,
|
||||
}
|
||||
}
|
||||
|
||||
func tgEmailSentCode(hash, emailPattern string, length int) tg.AuthSentCodeClass {
|
||||
if length <= 0 {
|
||||
length = devCodeLength
|
||||
|
|
@ -303,6 +313,8 @@ func (r *Router) tgSentCodeForHash(ctx context.Context, hash string) (tg.AuthSen
|
|||
return nil, signInErr(auth.ErrCodeExpired)
|
||||
}
|
||||
switch delivery.Kind {
|
||||
case domain.AuthCodeDeliverySMS:
|
||||
return tgSMSSentCode(hash, delivery.Length), nil
|
||||
case domain.AuthCodeDeliveryEmail:
|
||||
return tgEmailSentCode(hash, delivery.EmailPattern, delivery.Length), nil
|
||||
case domain.AuthCodeDeliveryEmailSetupRequired:
|
||||
|
|
@ -361,7 +373,16 @@ func (r *Router) finishAuthSignIn(ctx context.Context, u domain.User, loginMessa
|
|||
}
|
||||
|
||||
func (r *Router) onAuthResendCode(ctx context.Context, req *tg.AuthResendCodeRequest) (tg.AuthSentCodeClass, error) {
|
||||
hash, err := r.deps.Auth.ResendCode(ctx, req.PhoneNumber, req.PhoneCodeHash)
|
||||
var hash string
|
||||
var err error
|
||||
if scoped, ok := r.deps.Auth.(interface {
|
||||
ResendCodeForAuthKey(context.Context, [8]byte, string, string) (string, error)
|
||||
}); ok {
|
||||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||||
hash, err = scoped.ResendCodeForAuthKey(ctx, authKeyID, req.PhoneNumber, req.PhoneCodeHash)
|
||||
} else {
|
||||
hash, err = r.deps.Auth.ResendCode(ctx, req.PhoneNumber, req.PhoneCodeHash)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, signInErr(err)
|
||||
}
|
||||
|
|
@ -369,7 +390,16 @@ func (r *Router) onAuthResendCode(ctx context.Context, req *tg.AuthResendCodeReq
|
|||
}
|
||||
|
||||
func (r *Router) onAuthCancelCode(ctx context.Context, req *tg.AuthCancelCodeRequest) (bool, error) {
|
||||
if err := r.deps.Auth.CancelCode(ctx, req.PhoneNumber, req.PhoneCodeHash); err != nil {
|
||||
var err error
|
||||
if scoped, ok := r.deps.Auth.(interface {
|
||||
CancelCodeForAuthKey(context.Context, [8]byte, string, string) error
|
||||
}); ok {
|
||||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||||
err = scoped.CancelCodeForAuthKey(ctx, authKeyID, req.PhoneNumber, req.PhoneCodeHash)
|
||||
} else {
|
||||
err = r.deps.Auth.CancelCode(ctx, req.PhoneNumber, req.PhoneCodeHash)
|
||||
}
|
||||
if err != nil {
|
||||
return false, signInErr(err)
|
||||
}
|
||||
return true, nil
|
||||
|
|
|
|||
|
|
@ -232,6 +232,11 @@ func tgChannelUpdate(viewerUserID int64, event domain.ChannelUpdateEvent) tg.Upd
|
|||
|
||||
func tgOtherUpdateFromEvent(event domain.UpdateEvent) tg.UpdateClass {
|
||||
switch event.Type {
|
||||
case domain.UpdateEventUserPhone:
|
||||
if event.UserID == 0 || event.Phone == "" {
|
||||
return nil
|
||||
}
|
||||
return &tg.UpdateUserPhone{UserID: event.UserID, Phone: event.Phone}
|
||||
case domain.UpdateEventChannelState:
|
||||
if event.Peer.Type != domain.PeerTypeChannel || event.Peer.ID == 0 {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -254,6 +254,8 @@ type UserPremiumStatusService interface {
|
|||
|
||||
// AccountService 抽象账号设置查询。
|
||||
type AccountService interface {
|
||||
SendChangePhoneCode(ctx context.Context, userID int64, authKeyID [8]byte, sessionID int64, phone string) (string, domain.AuthCodeDelivery, error)
|
||||
ChangePhone(ctx context.Context, userID int64, authKeyID [8]byte, sessionID int64, phone, phoneCodeHash, code string, date int) (domain.PhoneChangeResult, error)
|
||||
GetPassword(ctx context.Context, userID int64) (domain.PasswordSettings, error)
|
||||
GetPasswordSettings(ctx context.Context, userID int64, check domain.PasswordCheck) (domain.PrivatePasswordSettings, error)
|
||||
UpdatePasswordSettings(ctx context.Context, userID int64, check domain.PasswordCheck, input domain.PasswordInputSettings) error
|
||||
|
|
|
|||
|
|
@ -295,8 +295,35 @@ func floodWaitErr(seconds int) error {
|
|||
// phoneNumberInvalidErr 表示手机号为空或格式非法(auth.sendCode/signIn/signUp)。
|
||||
func phoneNumberInvalidErr() error { return tgerr.New(406, "PHONE_NUMBER_INVALID") }
|
||||
|
||||
func phoneNumberOccupiedErr() error { return tgerr.New(400, "PHONE_NUMBER_OCCUPIED") }
|
||||
func phoneCodeEmptyErr() error { return tgerr.New(400, "PHONE_CODE_EMPTY") }
|
||||
func phoneCodeInvalidErr() error { return tgerr.New(400, "PHONE_CODE_INVALID") }
|
||||
func phoneCodeExpiredErr() error { return tgerr.New(400, "PHONE_CODE_EXPIRED") }
|
||||
|
||||
func phoneChangeErr(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrPhoneNumberInvalid):
|
||||
return phoneNumberInvalidErr()
|
||||
case errors.Is(err, domain.ErrPhoneNumberOccupied):
|
||||
return phoneNumberOccupiedErr()
|
||||
case errors.Is(err, domain.ErrPhoneCodeEmpty):
|
||||
return phoneCodeEmptyErr()
|
||||
case errors.Is(err, domain.ErrPhoneCodeInvalid):
|
||||
return phoneCodeInvalidErr()
|
||||
case errors.Is(err, domain.ErrPhoneCodeExpired):
|
||||
return phoneCodeExpiredErr()
|
||||
case errors.Is(err, domain.ErrPhoneChangeAuthInvalid):
|
||||
return authKeyUnregisteredErr()
|
||||
case errors.Is(err, domain.ErrPhoneChangeForbidden):
|
||||
return tgerr.New(400, "BOT_METHOD_INVALID")
|
||||
default:
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
|
||||
// authKeyUnregisteredErr 表示请求要求登录态而当前连接未授权。
|
||||
func authKeyUnregisteredErr() error { return tgerr.New(401, "AUTH_KEY_UNREGISTERED") }
|
||||
func botMethodInvalidErr() error { return tgerr.New(400, "BOT_METHOD_INVALID") }
|
||||
|
||||
// 私聊通话(phone.*)错误;触发点见 internal/rpc/phone_calls.go 与 app/phone 错误映射。
|
||||
func callPeerInvalidErr() error { return tgerr.New(400, "CALL_PEER_INVALID") }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
androidcompat "telesrv/internal/compat/android"
|
||||
"telesrv/internal/compat/tdesktop"
|
||||
)
|
||||
|
||||
|
|
@ -69,9 +70,31 @@ func (r *Router) registerHelp(d *tg.ServerDispatcher) {
|
|||
d.OnHelpGetDeepLinkInfo(func(ctx context.Context, path string) (tg.HelpDeepLinkInfoClass, error) {
|
||||
return &tg.HelpDeepLinkInfoEmpty{}, nil
|
||||
})
|
||||
d.OnHelpDismissSuggestion(r.onHelpDismissSuggestion)
|
||||
d.OnHelpGetPremiumPromo(r.onHelpGetPremiumPromo)
|
||||
}
|
||||
|
||||
// onHelpDismissSuggestion 为 DrKLO 改号成功后的 suggestion 清理提供有界兼容。
|
||||
// Android 会先把 suggestion 从本地状态删除,再发送该 RPC,且 generic 500 会被
|
||||
// 连接层持续重试。当前 server 不发布 pending suggestions,故非空 dismissal
|
||||
// 无需持久化,幂等 BoolTrue 即为完整的当前边界语义。
|
||||
func (r *Router) onHelpDismissSuggestion(ctx context.Context, req *tg.HelpDismissSuggestionRequest) (bool, error) {
|
||||
userID, found, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
if !found {
|
||||
return false, authKeyUnregisteredErr()
|
||||
}
|
||||
if r.userIsBot(ctx, userID) {
|
||||
return false, botMethodInvalidErr()
|
||||
}
|
||||
if req == nil {
|
||||
return false, nil
|
||||
}
|
||||
return androidcompat.DismissSuggestion(req.Suggestion), nil
|
||||
}
|
||||
|
||||
// onHelpGetPremiumPromo 返回最小真实的 Premium 状态页数据:状态文案按 viewer
|
||||
// 的会员有效期生成;videos/period_options 留空——购买入口已被 appConfig
|
||||
// premium_purchase_blocked=true 关闭,订阅价格 UI 不会消费这些字段(TDesktop
|
||||
|
|
|
|||
56
internal/rpc/help_dismiss_suggestion_test.go
Normal file
56
internal/rpc/help_dismiss_suggestion_test.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gotd/td/bin"
|
||||
"github.com/gotd/td/clock"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/gotd/td/tgerr"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
func TestHelpDismissSuggestionAndroidChangePhone(t *testing.T) {
|
||||
r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System)
|
||||
ctx := WithUserID(context.Background(), 42)
|
||||
req := &tg.HelpDismissSuggestionRequest{
|
||||
Peer: &tg.InputPeerEmpty{},
|
||||
Suggestion: "VALIDATE_PHONE_NUMBER",
|
||||
}
|
||||
var in bin.Buffer
|
||||
if err := req.Encode(&in); err != nil {
|
||||
t.Fatalf("encode: %v", err)
|
||||
}
|
||||
enc, err := r.Dispatch(ctx, [8]byte{1, 2, 3}, 77, &in)
|
||||
if err != nil {
|
||||
t.Fatalf("dispatch: %v", err)
|
||||
}
|
||||
box, ok := enc.(*tg.BoolBox)
|
||||
if !ok {
|
||||
t.Fatalf("response = %T, want *tg.BoolBox", enc)
|
||||
}
|
||||
if _, ok := box.Bool.(*tg.BoolTrue); !ok {
|
||||
t.Fatalf("bool response = %T, want BoolTrue", box.Bool)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpDismissSuggestionRequiresAuthorization(t *testing.T) {
|
||||
r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System)
|
||||
req := &tg.HelpDismissSuggestionRequest{Peer: &tg.InputPeerEmpty{}, Suggestion: "VALIDATE_PHONE_NUMBER"}
|
||||
var in bin.Buffer
|
||||
if err := req.Encode(&in); err != nil {
|
||||
t.Fatalf("encode: %v", err)
|
||||
}
|
||||
if _, err := r.Dispatch(context.Background(), [8]byte{1}, 77, &in); !tgerr.Is(err, "AUTH_KEY_UNREGISTERED") {
|
||||
t.Fatalf("unauthorized err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpDismissSuggestionEmptyIsFalse(t *testing.T) {
|
||||
r := &Router{}
|
||||
ok, err := r.onHelpDismissSuggestion(WithUserID(context.Background(), 42), &tg.HelpDismissSuggestionRequest{Peer: &tg.InputPeerEmpty{}})
|
||||
if err != nil || ok {
|
||||
t.Fatalf("empty suggestion result=%v err=%v", ok, err)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue