Sync telesrv 73f5c91 (feat(account): implement unified account deletion lifecycle). Skipped telesrv docs changes per public sync rules.
177 lines
6.4 KiB
Go
177 lines
6.4 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/iamxvbaba/td/clock"
|
|
"github.com/iamxvbaba/td/proto"
|
|
"github.com/iamxvbaba/td/tg"
|
|
"github.com/iamxvbaba/td/tgerr"
|
|
"go.uber.org/zap/zaptest"
|
|
|
|
appaccount "telesrv/internal/app/account"
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/postresponse"
|
|
"telesrv/internal/store/memory"
|
|
)
|
|
|
|
func TestAccountDeleteRPCDeliversResultBeforeClosingCurrentSession(t *testing.T) {
|
|
current := [8]byte{1}
|
|
other := [8]byte{2}
|
|
accountSvc := &rpcDeletionAccountService{
|
|
Service: appaccount.NewService(memory.NewPasswordStore()),
|
|
outcome: domain.AccountDeleteOutcome{
|
|
Kind: domain.AccountDeleteImmediate,
|
|
Deletion: domain.AccountDeletionResult{Changed: true, RevokedAuthorizations: []domain.Authorization{
|
|
{AuthKeyID: current, UserID: 42},
|
|
{AuthKeyID: other, UserID: 42},
|
|
}},
|
|
},
|
|
}
|
|
sessions := &deletionCaptureSessions{}
|
|
r := New(Config{}, Deps{Account: accountSvc, Sessions: sessions}, zaptest.NewLogger(t), clock.System)
|
|
ctx := postresponse.WithCallbacks(WithSessionID(WithAuthKeyID(WithUserID(context.Background(), 42), current), 77))
|
|
ok, err := r.onAccountDeleteAccount(ctx, &tg.AccountDeleteAccountRequest{Reason: "manual"})
|
|
if err != nil || !ok {
|
|
t.Fatalf("delete account ok=%v err=%v", ok, err)
|
|
}
|
|
if sessions.wasClosed(current) {
|
|
t.Fatal("current auth key closed before rpc_result delivery")
|
|
}
|
|
if !sessions.wasClosed(other) {
|
|
t.Fatal("other auth key was not revoked immediately")
|
|
}
|
|
postresponse.Run(ctx)
|
|
if !sessions.wasClosed(current) {
|
|
t.Fatal("current auth key not closed after rpc_result delivery")
|
|
}
|
|
}
|
|
|
|
func TestAccountDeleteRPCMapsDelayedTwoFAWait(t *testing.T) {
|
|
accountSvc := &rpcDeletionAccountService{
|
|
Service: appaccount.NewService(memory.NewPasswordStore()),
|
|
outcome: domain.AccountDeleteOutcome{Kind: domain.AccountDeleteDelayed, WaitSeconds: 604800},
|
|
}
|
|
r := New(Config{}, Deps{Account: accountSvc}, zaptest.NewLogger(t), clock.System)
|
|
ctx := WithAuthKeyID(WithUserID(context.Background(), 42), [8]byte{1})
|
|
ok, err := r.onAccountDeleteAccount(ctx, &tg.AccountDeleteAccountRequest{Reason: "Forgot password"})
|
|
if ok || !tgerr.Is(err, "2FA_CONFIRM_WAIT") || !strings.Contains(err.Error(), "604800") {
|
|
t.Fatalf("delayed delete ok=%v err=%v", ok, err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteAccountAllowedWithoutFullAuthorization(t *testing.T) {
|
|
if !rpcAllowedWithoutAuthorization(tg.AccountDeleteAccountRequestTypeID) {
|
|
t.Fatal("account.deleteAccount must reach the narrow password_pending identity resolver")
|
|
}
|
|
if rpcAllowedWithoutAuthorization(tg.AccountConfirmPhoneRequestTypeID) || rpcAllowedWithoutAuthorization(tg.AccountSendConfirmPhoneCodeRequestTypeID) {
|
|
t.Fatal("confirm-phone methods must remain fully authorized")
|
|
}
|
|
}
|
|
|
|
func TestAccountDeletionNotificationCompletesForOfflineTarget(t *testing.T) {
|
|
sessions := &offlineDeletionSessions{}
|
|
svc := &deletionWorkerService{}
|
|
r := New(Config{}, Deps{Sessions: sessions}, zaptest.NewLogger(t), clock.System)
|
|
r.dispatchAccountDeletionNotification(context.Background(), svc, domain.AccountDeletionNotification{
|
|
ID: 9, TargetUserID: 42, DeletedUserID: 77, Attempts: 1,
|
|
})
|
|
if len(svc.completed) != 1 || svc.completed[0] != 9 {
|
|
t.Fatalf("completed notifications = %v, want [9]", svc.completed)
|
|
}
|
|
}
|
|
|
|
func TestAccountLifecyclePartialSweepFinishesCommittedDeletion(t *testing.T) {
|
|
revoked := [8]byte{3}
|
|
svc := &rpcDeletionAccountService{
|
|
Service: appaccount.NewService(memory.NewPasswordStore()),
|
|
sweepResults: []domain.AccountDeletionResult{{
|
|
Changed: true,
|
|
User: domain.User{ID: 42, Deleted: true},
|
|
RevokedAuthorizations: []domain.Authorization{{AuthKeyID: revoked, UserID: 42}},
|
|
}},
|
|
sweepErr: errors.New("later candidate failed"),
|
|
}
|
|
sessions := &deletionCaptureSessions{}
|
|
r := New(Config{}, Deps{Account: svc, Sessions: sessions}, zaptest.NewLogger(t), clock.System)
|
|
r.runAccountLifecycleOnce(context.Background(), 10)
|
|
if !sessions.wasClosed(revoked) {
|
|
t.Fatal("committed deletion authorization was not closed after partial sweep failure")
|
|
}
|
|
}
|
|
|
|
type rpcDeletionAccountService struct {
|
|
*appaccount.Service
|
|
outcome domain.AccountDeleteOutcome
|
|
err error
|
|
sweepResults []domain.AccountDeletionResult
|
|
sweepErr error
|
|
}
|
|
|
|
func (s *rpcDeletionAccountService) DeleteAccount(context.Context, int64, [8]byte, string, *domain.PasswordCheck, time.Time) (domain.AccountDeleteOutcome, error) {
|
|
return s.outcome, s.err
|
|
}
|
|
|
|
func (*rpcDeletionAccountService) SendConfirmPhoneCode(context.Context, int64, [8]byte, int64, string) (string, domain.AuthCodeDelivery, error) {
|
|
return "hash", domain.AuthCodeDelivery{Kind: domain.AuthCodeDeliverySMS, Length: 5}, nil
|
|
}
|
|
|
|
func (*rpcDeletionAccountService) ConfirmPhone(context.Context, int64, [8]byte, string, string, time.Time) ([]domain.Authorization, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (*rpcDeletionAccountService) ResendConfirmPhoneCode(context.Context, int64, [8]byte, int64, string, string) (string, domain.AuthCodeDelivery, bool, error) {
|
|
return "", domain.AuthCodeDelivery{}, false, nil
|
|
}
|
|
|
|
func (*rpcDeletionAccountService) CancelConfirmPhoneCode(context.Context, int64, [8]byte, string, string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
func (s *rpcDeletionAccountService) SweepDueAccountDeletions(context.Context, time.Time, int) ([]domain.AccountDeletionResult, error) {
|
|
return s.sweepResults, s.sweepErr
|
|
}
|
|
|
|
type deletionCaptureSessions struct {
|
|
captureSessions
|
|
closed [][8]byte
|
|
}
|
|
|
|
type offlineDeletionSessions struct{ captureSessions }
|
|
|
|
func (*offlineDeletionSessions) PushToUserExceptAuthKeySession(context.Context, int64, [8]byte, int64, proto.MessageType, tg.UpdatesClass) (int, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
type deletionWorkerService struct{ completed []int64 }
|
|
|
|
func (*deletionWorkerService) SweepDueAccountDeletions(context.Context, time.Time, int) ([]domain.AccountDeletionResult, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (*deletionWorkerService) ClaimAccountDeletionNotifications(context.Context, time.Time, int, time.Duration) ([]domain.AccountDeletionNotification, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *deletionWorkerService) CompleteAccountDeletionNotification(_ context.Context, id int64, _ time.Time) error {
|
|
s.completed = append(s.completed, id)
|
|
return nil
|
|
}
|
|
|
|
func (s *deletionCaptureSessions) CloseSessionsForBusinessAuthKey(id [8]byte) int {
|
|
s.closed = append(s.closed, id)
|
|
return 1
|
|
}
|
|
|
|
func (s *deletionCaptureSessions) wasClosed(id [8]byte) bool {
|
|
for _, closed := range s.closed {
|
|
if closed == id {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|