From d4dd09e4956d99ee838f35eb4ca94c861addb41c Mon Sep 17 00:00:00 2001 From: Oleksii Kyslytsia Date: Fri, 13 Jun 2025 17:41:50 +0400 Subject: [PATCH 1/8] fix(qrlogin): fix Export method to handle all AuthExportLoginToken response types The Export method now properly handles all possible response types from AuthExportLoginToken: - AuthLoginToken: normal case, returns new token - AuthLoginTokenSuccess: token already accepted, returns error - AuthLoginTokenMigrateTo: migration needed, returns MigrationNeededError Previously only AuthLoginToken was handled, which could cause unexpected type errors. --- telegram/auth/qrlogin/qrlogin.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/telegram/auth/qrlogin/qrlogin.go b/telegram/auth/qrlogin/qrlogin.go index e306f3eebf..b5f2ca18cb 100644 --- a/telegram/auth/qrlogin/qrlogin.go +++ b/telegram/auth/qrlogin/qrlogin.go @@ -47,11 +47,20 @@ 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, but we're trying to export + return Token{}, errors.New("login token already accepted") + 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. From 7e2b58c54a2c29bae8df411d4a347f1c74dba83a Mon Sep 17 00:00:00 2001 From: Oleksii Kyslytsia Date: Fri, 13 Jun 2025 17:49:39 +0400 Subject: [PATCH 2/8] feat(qrlogin): add cSpell configuration for custom words --- .vscode/settings.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..b4e0290fc1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "cSpell.words": [ + "APIID", + "DCID", + "gotd", + "qrlogin" + ], + "cSpell.language": "en, en-US" +} From 9f81eb6cab49d7f6fd1f035afedb82b06fcacb43 Mon Sep 17 00:00:00 2001 From: Oleksii Kyslytsia Date: Fri, 13 Jun 2025 18:04:32 +0400 Subject: [PATCH 3/8] fix(qrlogin): fix AuthLoginTokenSuccess handling in Export method AuthLoginTokenSuccess indicates successful authentication, not an error. Return empty token instead of error when login token was already accepted. --- telegram/auth/qrlogin/qrlogin.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/telegram/auth/qrlogin/qrlogin.go b/telegram/auth/qrlogin/qrlogin.go index b5f2ca18cb..9e1b091b93 100644 --- a/telegram/auth/qrlogin/qrlogin.go +++ b/telegram/auth/qrlogin/qrlogin.go @@ -51,8 +51,9 @@ func (q QR) Export(ctx context.Context, exceptIDs ...int64) (Token, error) { case *tg.AuthLoginToken: return NewToken(t.Token, t.Expires), nil case *tg.AuthLoginTokenSuccess: - // Token was already accepted, but we're trying to export - return Token{}, errors.New("login token already accepted") + // 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{ From 6ab914d62bb51fac8a0ee94751d073f683ad7b8c Mon Sep 17 00:00:00 2001 From: Oleksii Kyslytsia Date: Fri, 13 Jun 2025 18:36:20 +0400 Subject: [PATCH 4/8] feat(qrlogin): enhance Export and Auth methods to handle AuthLoginTokenSuccess case --- telegram/auth/qrlogin/errors.go | 5 +++++ telegram/auth/qrlogin/qrlogin.go | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/telegram/auth/qrlogin/errors.go b/telegram/auth/qrlogin/errors.go index 6a7d017f13..44e340dfe3 100644 --- a/telegram/auth/qrlogin/errors.go +++ b/telegram/auth/qrlogin/errors.go @@ -1,11 +1,16 @@ package qrlogin import ( + "errors" "fmt" "github.com/gotd/td/tg" ) +// ErrAlreadyAuthenticated indicates that user is already authenticated +// and no new QR token is needed. +var ErrAlreadyAuthenticated = errors.New("already authenticated") + // MigrationNeededError reports that Telegram requested DC migration to continue login. type MigrationNeededError struct { MigrateTo *tg.AuthLoginTokenMigrateTo diff --git a/telegram/auth/qrlogin/qrlogin.go b/telegram/auth/qrlogin/qrlogin.go index 9e1b091b93..ece5829851 100644 --- a/telegram/auth/qrlogin/qrlogin.go +++ b/telegram/auth/qrlogin/qrlogin.go @@ -53,6 +53,7 @@ func (q QR) Export(ctx context.Context, exceptIDs ...int64) (Token, error) { case *tg.AuthLoginTokenSuccess: // Token was already accepted, authentication successful // Return empty token since no new token is needed + ctx.Done() return Token{}, nil case *tg.AuthLoginTokenMigrateTo: // Migration needed @@ -157,6 +158,13 @@ func (q QR) Auth( if err != nil { return nil, err } + + // If token is empty, it means AuthLoginTokenSuccess was returned + // and authentication is already complete + if token.String() == "" { + return q.Import(ctx) + } + timer := q.clock.Timer(until(token)) defer clock.StopTimer(timer) @@ -173,6 +181,13 @@ func (q QR) Auth( if err != nil { return nil, err } + + // If empty token, it means AuthLoginTokenSuccess was returned + if t.String() == "" { + // QR was scanned and accepted, break out of loop + break + } + token = t timer.Reset(until(token)) From d1e32a19e1b7c948926e0df3c4907b42d232679e Mon Sep 17 00:00:00 2001 From: Oleksii Kyslytsia Date: Fri, 13 Jun 2025 18:49:35 +0400 Subject: [PATCH 5/8] fix(qrlogin): remove unnecessary context cancellation in Export method for AuthLoginTokenSuccess case --- telegram/auth/qrlogin/qrlogin.go | 1 - 1 file changed, 1 deletion(-) diff --git a/telegram/auth/qrlogin/qrlogin.go b/telegram/auth/qrlogin/qrlogin.go index ece5829851..ca9c0b1e41 100644 --- a/telegram/auth/qrlogin/qrlogin.go +++ b/telegram/auth/qrlogin/qrlogin.go @@ -53,7 +53,6 @@ func (q QR) Export(ctx context.Context, exceptIDs ...int64) (Token, error) { case *tg.AuthLoginTokenSuccess: // Token was already accepted, authentication successful // Return empty token since no new token is needed - ctx.Done() return Token{}, nil case *tg.AuthLoginTokenMigrateTo: // Migration needed From 2fd1d02c551eecf7bfb4f39ed4ecb328a29d1f14 Mon Sep 17 00:00:00 2001 From: Oleksii Kyslytsia Date: Mon, 16 Jun 2025 22:38:27 +0400 Subject: [PATCH 6/8] test(qrlogin): improve test coverage for missing methods - Add tests for MigrationNeededError.Error method (0% -> 100%) - Add tests for OnLoginToken function (0% -> 100%) - Add tests for Token.Image method (0% -> 75%) - Add tests for Import method with migration scenarios (43.5% -> 82.6%) - Overall coverage improved from 67.0% to 84.6% --- telegram/auth/qrlogin/qrlogin_test.go | 134 ++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/telegram/auth/qrlogin/qrlogin_test.go b/telegram/auth/qrlogin/qrlogin_test.go index 2e1a475699..672d9795bc 100644 --- a/telegram/auth/qrlogin/qrlogin_test.go +++ b/telegram/auth/qrlogin/qrlogin_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/stretchr/testify/require" + "rsc.io/qr" "github.com/gotd/neo" @@ -185,3 +186,136 @@ 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) { + 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): + 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()) +} From a219beab30a3ccd1b1c8df37249be0b8d77735b3 Mon Sep 17 00:00:00 2001 From: Oleksii Kyslytsia Date: Tue, 17 Jun 2025 11:15:02 +0400 Subject: [PATCH 7/8] feat(qrlogin): add additional custom words to cSpell configuration --- .vscode/settings.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b4e0290fc1..ff3233f485 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,10 @@ "APIID", "DCID", "gotd", - "qrlogin" + "qrlogin", + "stretchr", + "testutil", + "tgmock" ], "cSpell.language": "en, en-US" } From 4c22747e9a0299b457f45084c3dbcbcbb5a7a5e7 Mon Sep 17 00:00:00 2001 From: Oleksii Kyslytsia Date: Wed, 18 Jun 2025 12:46:34 +0400 Subject: [PATCH 8/8] fix(qrlogin): wait for login signal when AuthLoginTokenSuccess is received When Export returns empty token (AuthLoginTokenSuccess case), Auth method should wait for the loggedIn signal before calling Import, ensuring proper synchronization with the login flow and fixing test failures on macOS Go 1.23. --- telegram/auth/qrlogin/qrlogin.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/telegram/auth/qrlogin/qrlogin.go b/telegram/auth/qrlogin/qrlogin.go index ca9c0b1e41..f943c4cca0 100644 --- a/telegram/auth/qrlogin/qrlogin.go +++ b/telegram/auth/qrlogin/qrlogin.go @@ -159,9 +159,14 @@ func (q QR) Auth( } // If token is empty, it means AuthLoginTokenSuccess was returned - // and authentication is already complete + // and authentication is already complete, but we should wait for the signal if token.String() == "" { - return q.Import(ctx) + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-loggedIn: + return q.Import(ctx) + } } timer := q.clock.Timer(until(token))