merged from gramsrv upstream

This commit is contained in:
onysd 2026-09-01 12:06:31 +03:00
parent 79c64ee916
commit 21a0856587
651 changed files with 54774 additions and 4590 deletions

View file

@ -73,6 +73,32 @@ func TestWebhookPhoneLoginUsesRandomSMSCode(t *testing.T) {
}
}
func TestWebhookPhoneLoginCanonicalizesNationalTrunkBeforeOTP(t *testing.T) {
ctx := context.Background()
sender := &captureOTPSender{}
svc := NewService(
memory.NewUserStore(),
memory.NewAuthorizationStore(),
memory.NewCodeStore(),
nil,
nil,
"fixed-code-must-not-leak",
WithPhoneCodeDelivery(sender, 6),
)
hash, err := svc.SendCode(ctx, "+98 0998 167 9461")
if err != nil {
t.Fatalf("SendCode: %v", err)
}
if len(sender.requests) != 1 || sender.requests[0].Recipient != "989981679461" {
t.Fatalf("OTP requests = %+v, want canonical Iran recipient", sender.requests)
}
_, _, needSignUp, err := svc.SignIn(ctx, domain.Authorization{}, "989981679461", hash, sender.requests[0].Code)
if err != nil || !needSignUp {
t.Fatalf("SignIn canonical variant needSignUp=%v err=%v", needSignUp, err)
}
}
func TestWebhookExistingAccountRejectionKeepsDurableAppCode(t *testing.T) {
ctx := context.Background()
users := memory.NewUserStore()

View file

@ -64,7 +64,10 @@ func validPhone(phone string) bool {
}
func systemUserLoginForbidden(u domain.User) bool {
return domain.IsSystemUserID(u.ID)
// Login-facing callers deliberately use the same non-enumerating error for
// reserved identities and irreversible tombstones. The durable authorization
// store repeats the deleted check under the user lock to close the TOCTOU gap.
return u.Deleted || domain.IsSystemUserID(u.ID)
}
func systemLoginPhoneForbidden(phone string) bool {
@ -270,12 +273,14 @@ func NewService(users store.UserStore, auths store.AuthorizationStore, codes sto
}
// BindTempAuthKey 校验并记录 TDesktop PFS temp→perm auth key 绑定。
func (s *Service) BindTempAuthKey(ctx context.Context, sessionID int64, binding domain.TempAuthKeyBinding) error {
func (s *Service) BindTempAuthKey(ctx context.Context, sessionID int64, binding domain.TempAuthKeyBinding) (domain.TempAuthKeyBindingResult, error) {
var validated domain.TempAuthKeyBindingResult
if s.authKeys != nil {
inner, protocolExpiresAt, err := s.validateBindTempAuthKey(ctx, sessionID, binding)
inner, protocolExpiresAt, result, err := s.validateBindTempAuthKey(ctx, sessionID, binding)
if err != nil {
return err
return domain.TempAuthKeyBindingResult{}, err
}
validated = result
binding.TempSessionID = inner.TempSessionID
// The bind request's expires_at is a signed client assertion. TDesktop
// intentionally adds a small grace interval, while Android derives its
@ -287,21 +292,22 @@ func (s *Service) BindTempAuthKey(ctx context.Context, sessionID int64, binding
// The edge may admit the frame immediately before the temporary key's
// absolute boundary and the encrypted proof may cross it. This is a temp-key
// rotation condition, never a destructive permanent-key proof failure.
return ErrTempAuthKeyEmpty
return domain.TempAuthKeyBindingResult{}, ErrTempAuthKeyEmpty
}
if s.tempKeys == nil {
return nil
return validated, nil
}
if err := s.tempKeys.Save(ctx, binding); err != nil {
result, err := s.tempKeys.SaveWithState(ctx, binding)
if err != nil {
if errors.Is(err, store.ErrTempAuthKeyAlreadyBound) {
return ErrTempAuthKeyAlreadyBound
return domain.TempAuthKeyBindingResult{}, ErrTempAuthKeyAlreadyBound
}
if errors.Is(err, store.ErrAuthKeyBindingInvalid) {
return s.classifyBindingStoreInvalid(ctx, binding)
return domain.TempAuthKeyBindingResult{}, s.classifyBindingStoreInvalid(ctx, binding)
}
return err
return domain.TempAuthKeyBindingResult{}, err
}
return nil
return result, nil
}
// ResolveAuthKey 将已绑定的 temp auth_key 解析为对应 perm auth_key。
@ -323,7 +329,7 @@ func (s *Service) ResolveAuthKey(ctx context.Context, authKeyID [8]byte) ([8]byt
// UserID 返回 auth_key 当前绑定的用户。未登录、或两步验证未完成时 found=false。
func (s *Service) UserID(ctx context.Context, authKeyID [8]byte) (int64, bool, error) {
if s == nil || s.auths == nil {
if s == nil || s.auths == nil || s.users == nil {
return 0, false, nil
}
a, found, err := s.auths.ByAuthKey(ctx, authKeyID)
@ -334,7 +340,13 @@ func (s *Service) UserID(ctx context.Context, authKeyID [8]byte) (int64, bool, e
// 两步验证未完成:业务鉴权视为未登录,仅允许 auth.checkPassword 继续。
return 0, false, nil
}
if domain.IsSystemUserID(a.UserID) {
u, userFound, err := s.users.ByID(ctx, a.UserID)
if err != nil {
return 0, false, err
}
if !userFound || systemUserLoginForbidden(u) {
// A stale row can exist only after an interrupted/legacy path. Fail closed
// before it reaches the Router auth cache and retire it opportunistically.
_ = s.auths.Delete(ctx, authKeyID)
return 0, false, nil
}
@ -344,14 +356,18 @@ func (s *Service) UserID(ctx context.Context, authKeyID [8]byte) (int64, bool, e
// PendingPasswordUserID 返回处于"待两步验证"状态的 auth_key 对应的用户。
// UserID 对 password_pending 的 auth_key 返回未登录,auth.checkPassword 借此仍能定位待验证用户。
func (s *Service) PendingPasswordUserID(ctx context.Context, authKeyID [8]byte) (int64, bool, error) {
if s == nil || s.auths == nil {
if s == nil || s.auths == nil || s.users == nil {
return 0, false, nil
}
a, found, err := s.auths.ByAuthKey(ctx, authKeyID)
if err != nil || !found || !a.PasswordPending {
return 0, false, err
}
if domain.IsSystemUserID(a.UserID) {
u, userFound, err := s.users.ByID(ctx, a.UserID)
if err != nil {
return 0, false, err
}
if !userFound || systemUserLoginForbidden(u) {
_ = s.auths.Delete(ctx, authKeyID)
return 0, false, nil
}
@ -359,13 +375,24 @@ func (s *Service) PendingPasswordUserID(ctx context.Context, authKeyID [8]byte)
}
// CompletePasswordSignIn 在两步验证通过后清除 password_pending,使 auth_key 转为完全授权。
func (s *Service) CompletePasswordSignIn(ctx context.Context, authKeyID [8]byte) error {
func (s *Service) CompletePasswordSignIn(ctx context.Context, authKeyID [8]byte, expectedUserID int64) error {
if s == nil || s.auths == nil {
return nil
}
if err := s.auths.MarkPasswordPassed(ctx, authKeyID); err != nil {
if expectedUserID == 0 {
return store.ErrAuthorizationStateChanged
}
if err := s.auths.MarkPasswordPassed(ctx, authKeyID, expectedUserID); err != nil {
return err
}
// Revalidate the active account after the CAS promotion before Router caches
// or binds the session. Account deletion can still linearize immediately
// after the update and must leave the caller unauthorized.
if userID, found, err := s.UserID(ctx, authKeyID); err != nil {
return err
} else if !found || userID != expectedUserID {
return ErrSystemUserLoginForbidden
}
// This is where a 2FA account's sign-in actually finishes — finishSignIn
// deliberately skipped the welcome message while password_pending.
if a, found, err := s.auths.ByAuthKey(ctx, authKeyID); err == nil && found {
@ -1423,7 +1450,11 @@ func (s *Service) AuthKeyClientInfo(ctx context.Context, authKeyID [8]byte) (dom
if s == nil || s.authKeys == nil || authKeyID == ([8]byte{}) {
return domain.AuthKeyClientInfo{}, false, nil
}
key, found, err := s.authKeys.Get(ctx, authKeyID)
// Client metadata is a read-only projection. The physical connection's
// first-frame Get and active-key heartbeat already own the durable orphan
// lease, so this path must not turn every init/profile read into another
// last_used_at write.
key, found, err := s.authKeys.Revalidate(ctx, authKeyID)
if err != nil || !found {
return domain.AuthKeyClientInfo{}, found, err
}
@ -1499,6 +1530,16 @@ func (s *Service) ResetAuthorizations(ctx context.Context, userID int64, keepAut
}
func (s *Service) bind(ctx context.Context, auth domain.Authorization, userID int64) error {
if s == nil || s.users == nil || s.auths == nil || userID == 0 {
return ErrSystemUserLoginForbidden
}
u, found, err := s.users.ByID(ctx, userID)
if err != nil {
return err
}
if !found || systemUserLoginForbidden(u) {
return ErrSystemUserLoginForbidden
}
if s.authKeys != nil {
key, found, err := s.authKeys.Get(ctx, auth.AuthKeyID)
if err != nil {
@ -1519,6 +1560,9 @@ func (s *Service) bind(ctx context.Context, auth domain.Authorization, userID in
if errors.Is(err, store.ErrAuthKeyNotPermanent) {
return ErrAuthKeyPermEmpty
}
if errors.Is(err, domain.ErrAccountDeleted) || errors.Is(err, domain.ErrUserNotFound) {
return ErrSystemUserLoginForbidden
}
return err
}
return nil
@ -1535,17 +1579,19 @@ func (s *Service) passwordNeeded(ctx context.Context, userID int64) (bool, error
return found && settings.HasPassword, nil
}
const loginMessageTpl = `Login code: %s. Do not give this code to anyone, even if they say they are from ` + branding.ProductName + `!
func loginMessageTemplate() string {
return `Login code: %s. Do not give this code to anyone, even if they say they are from ` + branding.ProductName + `!
This code can be used to log in to your ` + branding.ProductName + ` account. We never ask it for anything else.
If you didn't request this code by trying to log in on another device, simply ignore this message.`
}
func (s *Service) recordLoginMessage(ctx context.Context, userID int64, code string) (domain.Message, error) {
if s.messages == nil || s.dialogs == nil {
return domain.Message{}, nil
}
body := fmt.Sprintf(loginMessageTpl, code)
body := fmt.Sprintf(loginMessageTemplate(), code)
codeOffset := len("Login code: ")
msg, err := s.messages.Create(ctx, domain.Message{
OwnerUserID: userID,
@ -1595,53 +1641,58 @@ func (s *Service) recordWelcomeMessage(ctx context.Context, u domain.User) {
})
}
func (s *Service) validateBindTempAuthKey(ctx context.Context, sessionID int64, binding domain.TempAuthKeyBinding) (mtcrypto.BindAuthKeyInner, int, error) {
func (s *Service) validateBindTempAuthKey(ctx context.Context, sessionID int64, binding domain.TempAuthKeyBinding) (mtcrypto.BindAuthKeyInner, int, domain.TempAuthKeyBindingResult, error) {
if binding.ExpiresAt <= int(time.Now().Unix()) {
return mtcrypto.BindAuthKeyInner{}, 0, ErrExpiresAtInvalid
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrExpiresAtInvalid
}
temp, found, err := s.authKeys.Get(ctx, binding.TempAuthKeyID)
permID := authKeyIDFromInt64(binding.PermAuthKeyID)
pair, err := s.authKeys.LoadBindingKeys(ctx, binding.TempAuthKeyID, permID)
if err != nil {
return mtcrypto.BindAuthKeyInner{}, 0, err
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, err
}
// expires_at in auth.bindTempAuthKey is client-supplied and must only attest
// to a still-live binding. It may never create or reclassify a protocol key;
// the caller normalizes durable retention to this handshake-authoritative
// temp.ExpiresAt instead of trusting the client value.
if !found || temp.ExpiresAt <= int(time.Now().Unix()) {
return mtcrypto.BindAuthKeyInner{}, 0, ErrTempAuthKeyEmpty
if !pair.TemporaryFound || pair.Temporary.ExpiresAt <= int(time.Now().Unix()) {
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrTempAuthKeyEmpty
}
permID := authKeyIDFromInt64(binding.PermAuthKeyID)
perm, found, err := s.authKeys.Get(ctx, permID)
if err != nil {
return mtcrypto.BindAuthKeyInner{}, 0, err
}
if !found || perm.ExpiresAt != 0 {
return mtcrypto.BindAuthKeyInner{}, 0, ErrEncryptedMessageInvalid
if !pair.PermanentFound || pair.Permanent.ExpiresAt != 0 {
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrEncryptedMessageInvalid
}
inner, err := decryptBindAuthKeyInner(perm, binding.EncryptedMessage)
inner, err := decryptBindAuthKeyInner(pair.Permanent, binding.EncryptedMessage)
if err != nil {
return mtcrypto.BindAuthKeyInner{}, 0, ErrEncryptedMessageInvalid
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrEncryptedMessageInvalid
}
if inner.Nonce != binding.Nonce ||
inner.TempAuthKeyID != authKeyIDInt64(binding.TempAuthKeyID) ||
inner.PermAuthKeyID != binding.PermAuthKeyID ||
inner.TempSessionID != sessionID ||
inner.ExpiresAt != binding.ExpiresAt {
return mtcrypto.BindAuthKeyInner{}, 0, ErrEncryptedMessageInvalid
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrEncryptedMessageInvalid
}
if temp.ExpiresAt <= int(time.Now().Unix()) {
return mtcrypto.BindAuthKeyInner{}, 0, ErrTempAuthKeyEmpty
if pair.Temporary.ExpiresAt <= int(time.Now().Unix()) {
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrTempAuthKeyEmpty
}
return inner, temp.ExpiresAt, nil
layer, observationID, err := store.MergeAuthKeyLayerObservations(
pair.Temporary.Layer, pair.Temporary.LayerObservationID,
pair.Permanent.Layer, pair.Permanent.LayerObservationID,
)
if err != nil {
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, err
}
return inner, pair.Temporary.ExpiresAt, domain.TempAuthKeyBindingResult{
Layer: layer, LayerObservationID: observationID,
}, nil
}
func (s *Service) classifyBindingStoreInvalid(ctx context.Context, binding domain.TempAuthKeyBinding) error {
if s == nil || s.authKeys == nil {
return ErrEncryptedMessageInvalid
}
temp, found, err := s.authKeys.Get(ctx, binding.TempAuthKeyID)
temp, found, err := s.authKeys.Revalidate(ctx, binding.TempAuthKeyID)
if err != nil {
return err
}

View file

@ -48,7 +48,7 @@ func TestBindTempAuthKeyValidatesEncryptedMessage(t *testing.T) {
t.Fatalf("encrypt bind message: %v", err)
}
err = svc.BindTempAuthKey(ctx, sessionID, domain.TempAuthKeyBinding{
_, err = svc.BindTempAuthKey(ctx, sessionID, domain.TempAuthKeyBinding{
TempAuthKeyID: tempKey.ID,
PermAuthKeyID: permKey.IntID(),
Nonce: nonce,
@ -59,7 +59,7 @@ func TestBindTempAuthKeyValidatesEncryptedMessage(t *testing.T) {
t.Fatalf("BindTempAuthKey valid message: %v", err)
}
err = svc.BindTempAuthKey(ctx, sessionID+1, domain.TempAuthKeyBinding{
_, err = svc.BindTempAuthKey(ctx, sessionID+1, domain.TempAuthKeyBinding{
TempAuthKeyID: tempKey.ID,
PermAuthKeyID: permKey.IntID(),
Nonce: nonce,
@ -89,7 +89,7 @@ func TestBindTempAuthKeyValidatesEncryptedMessage(t *testing.T) {
if err != nil {
t.Fatalf("encrypt extended bind message: %v", err)
}
err = svc.BindTempAuthKey(ctx, sessionID, domain.TempAuthKeyBinding{
_, err = svc.BindTempAuthKey(ctx, sessionID, domain.TempAuthKeyBinding{
TempAuthKeyID: tempKey.ID,
PermAuthKeyID: permKey.IntID(),
Nonce: nonce,
@ -120,17 +120,17 @@ func TestBindTempAuthKeyClassifiesExpiryWithoutDestroyingPermanentKey(t *testing
PermAuthKeyID: permKey.IntID(),
ExpiresAt: int(time.Now().Add(time.Hour).Unix()),
}
if err := svc.BindTempAuthKey(ctx, 1001, request); !errors.Is(err, ErrTempAuthKeyEmpty) {
if _, err := svc.BindTempAuthKey(ctx, 1001, request); !errors.Is(err, ErrTempAuthKeyEmpty) {
t.Fatalf("expired protocol temp key err = %v, want ErrTempAuthKeyEmpty", err)
}
request.TempAuthKeyID = testAuthKey(0x33).ID
if err := svc.BindTempAuthKey(ctx, 1001, request); !errors.Is(err, ErrTempAuthKeyEmpty) {
if _, err := svc.BindTempAuthKey(ctx, 1001, request); !errors.Is(err, ErrTempAuthKeyEmpty) {
t.Fatalf("missing protocol temp key err = %v, want ErrTempAuthKeyEmpty", err)
}
request.ExpiresAt = int(time.Now().Add(-time.Second).Unix())
if err := svc.BindTempAuthKey(ctx, 1001, request); !errors.Is(err, ErrExpiresAtInvalid) {
if _, err := svc.BindTempAuthKey(ctx, 1001, request); !errors.Is(err, ErrExpiresAtInvalid) {
t.Fatalf("expired request proof err = %v, want ErrExpiresAtInvalid", err)
}
}
@ -346,6 +346,59 @@ func TestAuthorizationBindRejectsTemporaryProtocolKey(t *testing.T) {
}
}
func TestDeletedUserCannotCrossAuthorizationBoundaries(t *testing.T) {
ctx := context.Background()
users := memory.NewUserStore()
authz := memory.NewAuthorizationStore()
deleted, err := users.Create(ctx, domain.User{
Deleted: true,
DeletedAt: time.Now().Unix(),
DeletionSource: domain.AccountDeletionManual,
})
if err != nil {
t.Fatalf("create deleted user: %v", err)
}
svc := NewService(users, authz, memory.NewCodeStore(), nil, nil, "12345")
passkeyAuthKeyID := [8]byte{0x91}
if _, err := svc.BindVerifiedLogin(ctx, domain.Authorization{AuthKeyID: passkeyAuthKeyID}, deleted.ID); !errors.Is(err, ErrSystemUserLoginForbidden) {
t.Fatalf("BindVerifiedLogin deleted user err = %v, want ErrSystemUserLoginForbidden", err)
}
if _, found, err := authz.ByAuthKey(ctx, passkeyAuthKeyID); err != nil || found {
t.Fatalf("deleted passkey authorization found=%v err=%v, want absent", found, err)
}
qrAuthKeyID := [8]byte{0x92}
if _, err := svc.AcceptLoginToken(ctx, domain.Authorization{AuthKeyID: qrAuthKeyID}, deleted.ID); !errors.Is(err, ErrSystemUserLoginForbidden) {
t.Fatalf("AcceptLoginToken deleted user err = %v, want ErrSystemUserLoginForbidden", err)
}
if _, found, err := authz.ByAuthKey(ctx, qrAuthKeyID); err != nil || found {
t.Fatalf("deleted QR authorization found=%v err=%v, want absent", found, err)
}
staleAuthKeyID := [8]byte{0x93}
if err := authz.Bind(ctx, domain.Authorization{AuthKeyID: staleAuthKeyID, UserID: deleted.ID}); err != nil {
t.Fatalf("seed stale authorization: %v", err)
}
if userID, found, err := svc.UserID(ctx, staleAuthKeyID); err != nil || found || userID != 0 {
t.Fatalf("UserID stale tombstone = %d found=%v err=%v, want unauthorized", userID, found, err)
}
if _, found, err := authz.ByAuthKey(ctx, staleAuthKeyID); err != nil || found {
t.Fatalf("stale tombstone authorization found=%v err=%v, want retired", found, err)
}
pendingAuthKeyID := [8]byte{0x94}
if err := authz.Bind(ctx, domain.Authorization{AuthKeyID: pendingAuthKeyID, UserID: deleted.ID, PasswordPending: true}); err != nil {
t.Fatalf("seed stale pending authorization: %v", err)
}
if err := svc.CompletePasswordSignIn(ctx, pendingAuthKeyID, deleted.ID); !errors.Is(err, ErrSystemUserLoginForbidden) {
t.Fatalf("CompletePasswordSignIn deleted user err = %v, want ErrSystemUserLoginForbidden", err)
}
if _, found, err := authz.ByAuthKey(ctx, pendingAuthKeyID); err != nil || found {
t.Fatalf("stale pending tombstone authorization found=%v err=%v, want retired", found, err)
}
}
func TestPhoneCodeAcceptsTDesktopDigitsOnlySignIn(t *testing.T) {
ctx := context.Background()
users := memory.NewUserStore()
@ -377,6 +430,100 @@ func TestPhoneCodeAcceptsTDesktopDigitsOnlySignIn(t *testing.T) {
}
}
func TestVirtual888PhoneRegistersAndSignsIn(t *testing.T) {
ctx := context.Background()
users := memory.NewUserStore()
authz := memory.NewAuthorizationStore()
delivery := &captureLoginCodeDelivery{}
svc := NewService(
users,
authz,
memory.NewCodeStore(),
nil,
nil,
"12345",
WithLoginCodeDelivery(delivery),
)
const (
formatted = "+888 12-34"
canonical = "8881234"
)
firstHash, err := svc.SendCode(ctx, formatted)
if err != nil {
t.Fatalf("SendCode virtual phone: %v", err)
}
verifyCodeForSignUp(t, svc, canonical, firstHash, "12345")
created, _, err := svc.SignUp(ctx, domain.Authorization{AuthKeyID: [8]byte{0x54}}, formatted, firstHash, "Virtual", "User")
if err != nil {
t.Fatalf("SignUp virtual phone: %v", err)
}
if created.Phone != canonical {
t.Fatalf("created phone = %q, want %q", created.Phone, canonical)
}
secondHash, err := svc.SendCode(ctx, canonical)
if err != nil {
t.Fatalf("SendCode existing virtual phone: %v", err)
}
signedIn, _, needSignUp, err := svc.SignIn(ctx, domain.Authorization{AuthKeyID: [8]byte{0x55}}, formatted, secondHash, "12345")
if err != nil {
t.Fatalf("SignIn virtual phone: %v", err)
}
if needSignUp || signedIn.ID != created.ID {
t.Fatalf("SignIn user=%d needSignUp=%v, want existing user %d", signedIn.ID, needSignUp, created.ID)
}
}
func TestIranNationalTrunkVariantsShareOneAccountIdentity(t *testing.T) {
ctx := context.Background()
users := memory.NewUserStore()
authz := memory.NewAuthorizationStore()
delivery := &captureLoginCodeDelivery{}
svc := NewService(
users,
authz,
memory.NewCodeStore(),
nil,
nil,
"12345",
WithLoginCodeDelivery(delivery),
)
const (
withNationalTrunk = "+98 0998 167 9461"
international = "989981679461"
canonical = "989981679461"
)
firstHash, err := svc.SendCode(ctx, withNationalTrunk)
if err != nil {
t.Fatalf("SendCode trunk variant: %v", err)
}
verifyCodeForSignUp(t, svc, international, firstHash, "12345")
created, _, err := svc.SignUp(ctx, domain.Authorization{AuthKeyID: [8]byte{1}}, international, firstHash, "Iran", "User")
if err != nil {
t.Fatalf("SignUp international variant: %v", err)
}
if created.Phone != canonical {
t.Fatalf("created phone = %q, want %q", created.Phone, canonical)
}
secondHash, err := svc.SendCode(ctx, international)
if err != nil {
t.Fatalf("SendCode existing international variant: %v", err)
}
signedIn, _, needSignUp, err := svc.SignIn(ctx, domain.Authorization{AuthKeyID: [8]byte{2}}, withNationalTrunk, secondHash, "12345")
if err != nil {
t.Fatalf("SignIn trunk variant: %v", err)
}
if needSignUp || signedIn.ID != created.ID {
t.Fatalf("SignIn user=%d needSignUp=%v, want existing user %d", signedIn.ID, needSignUp, created.ID)
}
if got, found, err := users.ByPhone(ctx, canonical); err != nil || !found || got.ID != created.ID {
t.Fatalf("canonical lookup user=%+v found=%v err=%v", got, found, err)
}
}
func verifyCodeForSignUp(t *testing.T, svc *Service, phone, hash, code string) {
t.Helper()
got, msg, needSignUp, err := svc.SignIn(context.Background(), domain.Authorization{}, phone, hash, code)
@ -809,7 +956,10 @@ func TestSignInExistingTwoFactorAccountNeedsPassword(t *testing.T) {
t.Fatalf("PendingPasswordUserID = %d pending=%v err=%v, want %d", pendingUID, pending, err, u.ID)
}
// 两步验证通过后转为完全授权。
if err := svc.CompletePasswordSignIn(ctx, key); err != nil {
if err := svc.CompletePasswordSignIn(ctx, key, 0); !errors.Is(err, store.ErrAuthorizationStateChanged) {
t.Fatalf("CompletePasswordSignIn without expected user err=%v, want authorization state changed", err)
}
if err := svc.CompletePasswordSignIn(ctx, key, u.ID); err != nil {
t.Fatalf("CompletePasswordSignIn: %v", err)
}
bound, found, err = svc.UserID(ctx, key)

View file

@ -151,7 +151,7 @@ func TestTwoFactorSignInDefersWelcomeMessageUntilPasswordCompletes(t *testing.T)
t.Fatalf("welcome message fired before password check completed: %+v", pending.Messages[0])
}
if err := svc.CompletePasswordSignIn(ctx, key); err != nil {
if err := svc.CompletePasswordSignIn(ctx, key, u.ID); err != nil {
t.Fatalf("CompletePasswordSignIn: %v", err)
}