gotd: handle all login token response types in QR login

Cherry-picked from https://github.com/gotd/td/commit/4c22747e9a0299b457f45084c3dbcbcbb5a7a5e7
This commit is contained in:
Oleksii Kyslytsia
2025-12-03 15:11:49 +02:00
committed by Tulir Asokan
parent 097211cba1
commit 09185e8e53
3 changed files with 177 additions and 5 deletions
+32 -3
View File
@@ -47,11 +47,21 @@ func (q QR) Export(ctx context.Context, exceptIDs ...int64) (Token, error) {
return Token{}, errors.Wrap(err, "export")
}
t, ok := result.(*tg.AuthLoginToken)
if !ok {
switch t := result.(type) {
case *tg.AuthLoginToken:
return NewToken(t.Token, t.Expires), nil
case *tg.AuthLoginTokenSuccess:
// Token was already accepted, authentication successful
// Return empty token since no new token is needed
return Token{}, nil
case *tg.AuthLoginTokenMigrateTo:
// Migration needed
return Token{}, &MigrationNeededError{
MigrateTo: t,
}
default:
return Token{}, errors.Errorf("unexpected type %T", result)
}
return NewToken(t.Token, t.Expires), nil
}
// Accept accepts given token.
@@ -147,6 +157,18 @@ func (q QR) Auth(
if err != nil {
return nil, err
}
// If token is empty, it means AuthLoginTokenSuccess was returned
// and authentication is already complete, but we should wait for the signal.
if token.Empty() {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-loggedIn:
return q.Import(ctx)
}
}
timer := q.clock.Timer(until(token))
defer clock.StopTimer(timer)
@@ -163,6 +185,13 @@ func (q QR) Auth(
if err != nil {
return nil, err
}
if t.Empty() {
// If empty token, it means AuthLoginTokenSuccess was returned.
// QR was scanned and accepted, break to import.
break
}
token = t
timer.Reset(until(token))
+140 -2
View File
@@ -2,12 +2,13 @@ package qrlogin
import (
"context"
"runtime"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/gotd/neo"
"github.com/stretchr/testify/require"
"rsc.io/qr"
"go.mau.fi/mautrix-telegram/pkg/gotd/constant"
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
@@ -185,3 +186,140 @@ func TestQR_Auth(t *testing.T) {
a.NoError(<-done)
}
func TestMigrationNeededError_Error(t *testing.T) {
a := require.New(t)
err := &MigrationNeededError{
MigrateTo: &tg.AuthLoginTokenMigrateTo{
DCID: 2,
},
}
a.Equal("migration to 2 needed", err.Error())
}
// Mock dispatcher that implements the required interface.
type mockDispatcher struct {
handler tg.LoginTokenHandler
}
func (m *mockDispatcher) OnLoginToken(h tg.LoginTokenHandler) {
m.handler = h
}
func TestOnLoginToken(t *testing.T) {
if runtime.GOOS == "darwin" {
t.Skip("skipping on macOS")
}
a := require.New(t)
dispatcher := &mockDispatcher{}
loggedIn := OnLoginToken(dispatcher)
// Verify that handler was set.
a.NotNil(dispatcher.handler)
// Test the handler
ctx := context.Background()
entities := tg.Entities{}
update := &tg.UpdateLoginToken{}
// First call should send to channel.
done := make(chan error, 1)
go func() {
done <- dispatcher.handler(ctx, entities, update)
}()
// Should receive signal.
select {
case <-loggedIn:
// Good
case <-time.After(time.Second * 5):
t.Fatal("should receive signal")
}
// Handler should return nil.
a.NoError(<-done)
// Second call when channel is full should not block.
err := dispatcher.handler(ctx, entities, update)
a.NoError(err)
}
func TestToken_Image(t *testing.T) {
a := require.New(t)
token := NewToken([]byte("test_token"), int(time.Now().Unix()))
// Test with valid QR level.
img, err := token.Image(qr.L)
a.NoError(err)
a.NotNil(img)
// Test with different QR levels.
levels := []qr.Level{qr.L, qr.M, qr.Q, qr.H}
for _, level := range levels {
img, err := token.Image(level)
a.NoError(err)
a.NotNil(img)
}
}
func TestQR_Import_WithMigration(t *testing.T) {
ctx := context.Background()
a := require.New(t)
// Test with migration function.
migrateCalled := false
migrate := func(ctx context.Context, dcID int) error {
migrateCalled = true
a.Equal(2, dcID)
return nil
}
mock, qr := testQR(t, migrate)
auth := &tg.AuthAuthorization{
User: &tg.User{ID: 10},
}
// First call returns migration needed.
mock.ExpectCall(&tg.AuthExportLoginTokenRequest{
APIID: constant.TestAppID,
APIHash: constant.TestAppHash,
}).ThenResult(&tg.AuthLoginTokenMigrateTo{
DCID: 2,
Token: testToken.token,
}).ExpectCall(&tg.AuthImportLoginTokenRequest{
Token: testToken.token,
}).ThenResult(&tg.AuthLoginTokenSuccess{
Authorization: auth,
})
result, err := qr.Import(ctx)
a.NoError(err)
a.Equal(auth, result)
a.True(migrateCalled)
}
func TestQR_Import_MigrationError(t *testing.T) {
ctx := context.Background()
a := require.New(t)
// Test with migration function that returns error,
migrate := func(ctx context.Context, dcID int) error {
return testutil.TestError()
}
mock, qr := testQR(t, migrate)
mock.ExpectCall(&tg.AuthExportLoginTokenRequest{
APIID: constant.TestAppID,
APIHash: constant.TestAppHash,
}).ThenResult(&tg.AuthLoginTokenMigrateTo{
DCID: 2,
})
_, err := qr.Import(ctx)
a.ErrorIs(err, testutil.TestError())
}
+5
View File
@@ -59,6 +59,11 @@ func (t Token) String() string {
return base64.URLEncoding.EncodeToString(t.token)
}
// Empty reports whether token is empty.
func (t Token) Empty() bool {
return len(t.token) == 0
}
// URL returns login URL.
//
// See https://core.telegram.org/api/qr-login#exporting-a-login-token.