fix: sync harden auth, privacy, and PTS state
This commit is contained in:
parent
c1597696af
commit
2512eab51d
24 changed files with 817 additions and 154 deletions
|
|
@ -746,7 +746,7 @@ func (r *Router) onAccountVerifyEmail(ctx context.Context, req *tg.AccountVerify
|
|||
if ClientTypeFrom(ctx) == ClientTypeAndroid {
|
||||
return &tg.AccountEmailVerifiedLogin{
|
||||
Email: email,
|
||||
SentCode: tgEmailSentCode(p.PhoneCodeHash, domain.MaskEmail(email), len(strings.TrimSpace(code))),
|
||||
SentCode: tgEmailSentCode(p.PhoneCodeHash, domain.MaskEmail(email), len(strings.TrimSpace(code)), r.loginEmailResetAvailable()),
|
||||
}, nil
|
||||
}
|
||||
u, _, needSignUp, signInErr := r.deps.Auth.SignInWithEmail(ctx, r.authzFromCtx(ctx), p.PhoneNumber, p.PhoneCodeHash, code)
|
||||
|
|
|
|||
|
|
@ -433,7 +433,7 @@ func tgSMSSentCode(hash string, length int) tg.AuthSentCodeClass {
|
|||
}
|
||||
}
|
||||
|
||||
func tgEmailSentCode(hash, emailPattern string, length int) tg.AuthSentCodeClass {
|
||||
func tgEmailSentCode(hash, emailPattern string, length int, resetAvailable bool) tg.AuthSentCodeClass {
|
||||
if length <= 0 {
|
||||
length = devCodeLength
|
||||
}
|
||||
|
|
@ -441,15 +441,26 @@ func tgEmailSentCode(hash, emailPattern string, length int) tg.AuthSentCodeClass
|
|||
EmailPattern: emailPattern,
|
||||
Length: length,
|
||||
}
|
||||
// reset_available_period=0 表示可立即调用 auth.resetLoginEmail(开发环境无等待期),
|
||||
// 让客户端的"无法访问邮箱?"逃生入口可用。
|
||||
codeType.SetResetAvailablePeriod(0)
|
||||
if resetAvailable {
|
||||
// 0 means the SMS fallback is available immediately. Absence means this
|
||||
// deployment cannot safely service auth.resetLoginEmail.
|
||||
codeType.SetResetAvailablePeriod(0)
|
||||
}
|
||||
return &tg.AuthSentCode{
|
||||
Type: codeType,
|
||||
PhoneCodeHash: hash,
|
||||
}
|
||||
}
|
||||
|
||||
type loginEmailResetAvailabilityChecker interface {
|
||||
LoginEmailResetAvailable() bool
|
||||
}
|
||||
|
||||
func (r *Router) loginEmailResetAvailable() bool {
|
||||
checker, ok := r.deps.Auth.(loginEmailResetAvailabilityChecker)
|
||||
return ok && checker.LoginEmailResetAvailable()
|
||||
}
|
||||
|
||||
func tgEmailSetupRequiredSentCode(hash string) tg.AuthSentCodeClass {
|
||||
return &tg.AuthSentCode{
|
||||
Type: &tg.AuthSentCodeTypeSetUpEmailRequired{},
|
||||
|
|
@ -472,7 +483,7 @@ func (r *Router) tgSentCodeForHash(ctx context.Context, hash string) (tg.AuthSen
|
|||
case domain.AuthCodeDeliverySMS:
|
||||
return tgSMSSentCode(hash, delivery.Length), nil
|
||||
case domain.AuthCodeDeliveryEmail:
|
||||
return tgEmailSentCode(hash, delivery.EmailPattern, delivery.Length), nil
|
||||
return tgEmailSentCode(hash, delivery.EmailPattern, delivery.Length, r.loginEmailResetAvailable()), nil
|
||||
case domain.AuthCodeDeliveryEmailSetupRequired:
|
||||
return tgEmailSetupRequiredSentCode(hash), nil
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -47,6 +47,35 @@ func TestEmailSentCodeUsesDeliveryLength(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEmailSentCodeAdvertisesResetOnlyWhenAvailable(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
available bool
|
||||
}{
|
||||
{name: "unavailable"},
|
||||
{name: "available", available: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
authSvc := &captureAuthService{
|
||||
codeDelivery: domain.AuthCodeDelivery{
|
||||
Kind: domain.AuthCodeDeliveryEmail, EmailPattern: "a***e@example.test", Length: 6,
|
||||
},
|
||||
resetAvailable: tc.available,
|
||||
}
|
||||
r := New(Config{}, Deps{Auth: authSvc}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700000000, 0)})
|
||||
sent, err := r.tgSentCodeForHash(context.Background(), "hash-email-reset")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
emailType := sent.(*tg.AuthSentCode).Type.(*tg.AuthSentCodeTypeEmailCode)
|
||||
_, present := emailType.GetResetAvailablePeriod()
|
||||
if present != tc.available {
|
||||
t.Fatalf("reset_available_period present=%v, want %v", present, tc.available)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthSignInRoutesOfficialEmailCodeCarriers(t *testing.T) {
|
||||
const (
|
||||
phone = "+86 188 0000 0021"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package rpc
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/iamxvbaba/td/clock"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
|
|
@ -10,9 +11,21 @@ import (
|
|||
|
||||
appaccount "telesrv/internal/app/account"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/otpdelivery"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
type capturePasswordRecoveryMailSender struct {
|
||||
to string
|
||||
code string
|
||||
}
|
||||
|
||||
func (s *capturePasswordRecoveryMailSender) Deliver(_ context.Context, req otpdelivery.Request) (otpdelivery.Result, error) {
|
||||
s.to = req.Recipient
|
||||
s.code = req.Code
|
||||
return otpdelivery.Result{}, nil
|
||||
}
|
||||
|
||||
func TestAccountGetPasswordUsesPendingPasswordUser(t *testing.T) {
|
||||
ctx := pendingPasswordContext()
|
||||
const userID int64 = 42
|
||||
|
|
@ -65,9 +78,11 @@ func TestAuthRecoverPasswordCompletesPendingSignIn(t *testing.T) {
|
|||
pendingPassword: true,
|
||||
}
|
||||
sessions := &captureSessions{}
|
||||
sender := &capturePasswordRecoveryMailSender{}
|
||||
router := New(Config{}, Deps{
|
||||
Auth: auth,
|
||||
Account: appaccount.NewService(passwords),
|
||||
Auth: auth,
|
||||
Account: appaccount.NewService(passwords,
|
||||
appaccount.WithLoginEmailVerification(memory.NewCodeStore(), sender, time.Minute, 3, 6)),
|
||||
Users: staticUsersService{user: domain.User{ID: userID, AccessHash: 7, Phone: "15550000042", FirstName: "Alice"}},
|
||||
Sessions: sessions,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
|
@ -75,7 +90,10 @@ func TestAuthRecoverPasswordCompletesPendingSignIn(t *testing.T) {
|
|||
if _, err := router.onAuthRequestPasswordRecovery(ctx); err != nil {
|
||||
t.Fatalf("auth.requestPasswordRecovery: %v", err)
|
||||
}
|
||||
if _, err := router.onAuthRecoverPassword(ctx, &tg.AuthRecoverPasswordRequest{Code: "12345"}); err != nil {
|
||||
if sender.to != "alice@example.com" || sender.code == "" {
|
||||
t.Fatalf("recovery delivery=%+v", sender)
|
||||
}
|
||||
if _, err := router.onAuthRecoverPassword(ctx, &tg.AuthRecoverPasswordRequest{Code: sender.code}); err != nil {
|
||||
t.Fatalf("auth.recoverPassword: %v", err)
|
||||
}
|
||||
if auth.completePasswordCount != 1 || auth.completedPasswordKey != authKeyID {
|
||||
|
|
|
|||
|
|
@ -479,6 +479,12 @@ func passwordErr(err error) error {
|
|||
return emailNotAllowedErr()
|
||||
case errors.Is(err, domain.ErrEmailCodeInvalid):
|
||||
return emailCodeInvalidErr()
|
||||
case errors.Is(err, domain.ErrRecoveryCodeEmpty):
|
||||
return tgerr.New(400, "CODE_EMPTY")
|
||||
case errors.Is(err, domain.ErrRecoveryCodeInvalid):
|
||||
return tgerr.New(400, "CODE_INVALID")
|
||||
case errors.Is(err, domain.ErrPasswordRecoveryExpired):
|
||||
return tgerr.New(400, "PASSWORD_RECOVERY_EXPIRED")
|
||||
case errors.Is(err, domain.ErrPasswordRecoveryNA):
|
||||
return passwordRecoveryNAErr()
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,22 @@ func TestPasswordErrMapsOccupiedLoginEmailToNotAllowed(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPasswordErrMapsRecoveryState(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{domain.ErrRecoveryCodeEmpty, "CODE_EMPTY"},
|
||||
{domain.ErrRecoveryCodeInvalid, "CODE_INVALID"},
|
||||
{domain.ErrPasswordRecoveryExpired, "PASSWORD_RECOVERY_EXPIRED"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if err := passwordErr(tc.err); !tgerr.Is(err, tc.want) {
|
||||
t.Fatalf("passwordErr(%v)=%v, want %s", tc.err, err, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindTempAuthKeyErrPreservesRecoverableRotationErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ type captureAuthService struct {
|
|||
signInWithEmailPhone string
|
||||
signInWithEmailHash string
|
||||
signInWithEmailCode string
|
||||
resetAvailable bool
|
||||
}
|
||||
|
||||
func (s *captureAuthService) LoginEmailResetAvailable() bool {
|
||||
return s.resetAvailable
|
||||
}
|
||||
|
||||
type blockingUserAuthService struct {
|
||||
|
|
|
|||
|
|
@ -249,15 +249,13 @@ func applyContactNoteToUserFull(user domain.User, full *tg.UserFull) bool {
|
|||
}
|
||||
|
||||
func (r *Router) buildUserFullProjection(ctx context.Context, currentUserID int64, u domain.User) (tg.UserFull, error) {
|
||||
visibility, err := r.userFullPrivacyVisibility(ctx, currentUserID, u.ID)
|
||||
if err != nil {
|
||||
return tg.UserFull{}, internalErr()
|
||||
}
|
||||
about := u.About
|
||||
if r.deps.Privacy != nil && u.ID != currentUserID {
|
||||
allowed, err := r.deps.Privacy.CanSee(ctx, u.ID, currentUserID, domain.PrivacyKeyAbout)
|
||||
if err != nil {
|
||||
return tg.UserFull{}, internalErr()
|
||||
}
|
||||
if !allowed {
|
||||
about = ""
|
||||
}
|
||||
if !visibility[domain.PrivacyKeyAbout] {
|
||||
about = ""
|
||||
}
|
||||
full := tg.UserFull{
|
||||
ID: u.ID,
|
||||
|
|
@ -279,24 +277,9 @@ func (r *Router) buildUserFullProjection(ctx context.Context, currentUserID int6
|
|||
// 通话入口:客户端不见 phone_calls_available=true 不显示通话按钮(P1 前置项)。
|
||||
// phone_calls_private 标记对端禁 P2P(p2p_allowed 真值在通话确认时另行计算)。
|
||||
if !u.Bot && u.ID != currentUserID {
|
||||
callsAllowed, p2pAllowed, voiceAllowed := true, true, true
|
||||
if r.deps.Privacy != nil {
|
||||
allowed, err := r.deps.Privacy.CanSee(ctx, u.ID, currentUserID, domain.PrivacyKeyPhoneCall)
|
||||
if err != nil {
|
||||
return tg.UserFull{}, internalErr()
|
||||
}
|
||||
callsAllowed = allowed
|
||||
allowed, err = r.deps.Privacy.CanSee(ctx, u.ID, currentUserID, domain.PrivacyKeyPhoneP2P)
|
||||
if err != nil {
|
||||
return tg.UserFull{}, internalErr()
|
||||
}
|
||||
p2pAllowed = allowed
|
||||
allowed, err = r.deps.Privacy.CanSee(ctx, u.ID, currentUserID, domain.PrivacyKeyVoiceMessages)
|
||||
if err != nil {
|
||||
return tg.UserFull{}, internalErr()
|
||||
}
|
||||
voiceAllowed = allowed
|
||||
}
|
||||
callsAllowed := visibility[domain.PrivacyKeyPhoneCall]
|
||||
p2pAllowed := visibility[domain.PrivacyKeyPhoneP2P]
|
||||
voiceAllowed := visibility[domain.PrivacyKeyVoiceMessages]
|
||||
full.PhoneCallsAvailable = callsAllowed
|
||||
full.VideoCallsAvailable = callsAllowed
|
||||
full.PhoneCallsPrivate = !p2pAllowed
|
||||
|
|
@ -326,15 +309,11 @@ func (r *Router) buildUserFullProjection(ctx context.Context, currentUserID int6
|
|||
break
|
||||
}
|
||||
}
|
||||
if err := r.fillUserFullPhotos(ctx, currentUserID, u.ID, &full); err != nil {
|
||||
if err := r.fillUserFullPhotos(ctx, currentUserID, u.ID, visibility[domain.PrivacyKeyProfilePhoto], &full); err != nil {
|
||||
return tg.UserFull{}, err
|
||||
}
|
||||
if r.deps.Account != nil {
|
||||
allowed, err := r.canSeeSavedMusic(ctx, currentUserID, u.ID)
|
||||
if err != nil {
|
||||
return tg.UserFull{}, err
|
||||
}
|
||||
if allowed {
|
||||
if visibility[domain.PrivacyKeySavedMusic] {
|
||||
music, err := r.deps.Account.ListSavedMusic(ctx, u.ID, 0, 1)
|
||||
if err != nil {
|
||||
return tg.UserFull{}, internalErr()
|
||||
|
|
@ -406,15 +385,7 @@ func (r *Router) buildUserFullProjection(ctx context.Context, currentUserID int6
|
|||
// 生日(account.updateBirthday):落 userFull.birthday,按 PrivacyKeyBirthday 对他人裁剪,
|
||||
// 本人恒可见。
|
||||
if u.Birthday.IsSet() {
|
||||
birthdayVisible := true
|
||||
if r.deps.Privacy != nil && u.ID != currentUserID {
|
||||
allowed, err := r.deps.Privacy.CanSee(ctx, u.ID, currentUserID, domain.PrivacyKeyBirthday)
|
||||
if err != nil {
|
||||
return tg.UserFull{}, internalErr()
|
||||
}
|
||||
birthdayVisible = allowed
|
||||
}
|
||||
if birthdayVisible {
|
||||
if visibility[domain.PrivacyKeyBirthday] {
|
||||
full.SetBirthday(tgBirthday(u.Birthday))
|
||||
}
|
||||
}
|
||||
|
|
@ -425,6 +396,51 @@ func (r *Router) buildUserFullProjection(ctx context.Context, currentUserID int6
|
|||
return full, nil
|
||||
}
|
||||
|
||||
// userFullPrivacyVisibility evaluates every privacy-controlled UserFull field
|
||||
// from one owner snapshot when the service supports batching. Older test or
|
||||
// alternate implementations retain scalar parity; a missing batch key fails
|
||||
// closed instead of exposing that field.
|
||||
func (r *Router) userFullPrivacyVisibility(ctx context.Context, viewerUserID, ownerUserID int64) (map[domain.PrivacyKey]bool, error) {
|
||||
keys := []domain.PrivacyKey{
|
||||
domain.PrivacyKeyAbout,
|
||||
domain.PrivacyKeyPhoneCall,
|
||||
domain.PrivacyKeyPhoneP2P,
|
||||
domain.PrivacyKeyVoiceMessages,
|
||||
domain.PrivacyKeyProfilePhoto,
|
||||
domain.PrivacyKeySavedMusic,
|
||||
domain.PrivacyKeyBirthday,
|
||||
}
|
||||
out := make(map[domain.PrivacyKey]bool, len(keys))
|
||||
if r.deps.Privacy == nil || ownerUserID == viewerUserID {
|
||||
for _, key := range keys {
|
||||
out[key] = true
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
if batch, ok := r.deps.Privacy.(batchPrivacyEvaluator); ok {
|
||||
matrix, err := batch.CanSeeBatch(ctx, []int64{ownerUserID}, viewerUserID, keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ownerVisibility, found := matrix[ownerUserID]
|
||||
if !found {
|
||||
return out, nil
|
||||
}
|
||||
for _, key := range keys {
|
||||
out[key] = ownerVisibility[key]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
for _, key := range keys {
|
||||
allowed, err := r.deps.Privacy.CanSee(ctx, ownerUserID, viewerUserID, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[key] = allowed
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// applyAccountRatingToUserFull projects gramsrv's stored composite rating through
|
||||
// the rating fields official clients already render. This is a gramsrv policy
|
||||
// score, not a promise that its inputs or thresholds match Telegram's service.
|
||||
|
|
@ -724,7 +740,7 @@ func savedMusicDocumentIDs(docs []domain.Document) []int64 {
|
|||
return ids
|
||||
}
|
||||
|
||||
func (r *Router) fillUserFullPhotos(ctx context.Context, viewerUserID, ownerUserID int64, full *tg.UserFull) error {
|
||||
func (r *Router) fillUserFullPhotos(ctx context.Context, viewerUserID, ownerUserID int64, profileAllowed bool, full *tg.UserFull) error {
|
||||
if r.deps.Files == nil || full == nil || ownerUserID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -756,14 +772,6 @@ func (r *Router) fillUserFullPhotos(ctx context.Context, viewerUserID, ownerUser
|
|||
}
|
||||
}
|
||||
}
|
||||
profileAllowed := true
|
||||
if r.deps.Privacy != nil {
|
||||
var err error
|
||||
profileAllowed, err = r.deps.Privacy.CanSee(ctx, ownerUserID, viewerUserID, domain.PrivacyKeyProfilePhoto)
|
||||
if err != nil {
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
if profileAllowed {
|
||||
if photo, found, err := r.deps.Files.CurrentProfilePhotoKind(ctx, domain.PeerTypeUser, ownerUserID, domain.ProfilePhotoKindProfile); err != nil {
|
||||
return internalErr()
|
||||
|
|
|
|||
122
internal/rpc/users_full_privacy_batch_test.go
Normal file
122
internal/rpc/users_full_privacy_batch_test.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/iamxvbaba/td/clock"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
type userFullBatchPrivacy struct {
|
||||
stubPrivacy
|
||||
visible map[domain.PrivacyKey]bool
|
||||
batchCalls int
|
||||
scalarCalls int
|
||||
keys []domain.PrivacyKey
|
||||
}
|
||||
|
||||
func (p *userFullBatchPrivacy) CanSee(_ context.Context, _, _ int64, key domain.PrivacyKey) (bool, error) {
|
||||
p.scalarCalls++
|
||||
return p.visible[key], nil
|
||||
}
|
||||
|
||||
func (p *userFullBatchPrivacy) CanSeeBatch(_ context.Context, ownerUserIDs []int64, _ int64, keys []domain.PrivacyKey) (map[int64]map[domain.PrivacyKey]bool, error) {
|
||||
p.batchCalls++
|
||||
p.keys = append([]domain.PrivacyKey(nil), keys...)
|
||||
out := make(map[int64]map[domain.PrivacyKey]bool, len(ownerUserIDs))
|
||||
for _, ownerID := range ownerUserIDs {
|
||||
owner := make(map[domain.PrivacyKey]bool, len(p.visible))
|
||||
for key, allowed := range p.visible {
|
||||
owner[key] = allowed
|
||||
}
|
||||
out[ownerID] = owner
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type userFullScalarPrivacy struct {
|
||||
stubPrivacy
|
||||
visible map[domain.PrivacyKey]bool
|
||||
calls int
|
||||
}
|
||||
|
||||
func (p *userFullScalarPrivacy) CanSee(_ context.Context, _, _ int64, key domain.PrivacyKey) (bool, error) {
|
||||
p.calls++
|
||||
return p.visible[key], nil
|
||||
}
|
||||
|
||||
func TestBuildUserFullProjectionBatchesPrivacyAndFailsClosedOnMissingKeys(t *testing.T) {
|
||||
privacy := &userFullBatchPrivacy{visible: map[domain.PrivacyKey]bool{
|
||||
domain.PrivacyKeyAbout: false,
|
||||
domain.PrivacyKeyPhoneCall: true,
|
||||
domain.PrivacyKeyPhoneP2P: false,
|
||||
domain.PrivacyKeyVoiceMessages: false,
|
||||
domain.PrivacyKeyBirthday: true,
|
||||
// ProfilePhoto and SavedMusic are deliberately absent: batch omissions
|
||||
// must stay denied rather than becoming a privacy bypass.
|
||||
}}
|
||||
r := New(Config{}, Deps{Privacy: privacy}, zaptest.NewLogger(t), clock.System)
|
||||
full, err := r.buildUserFullProjection(context.Background(), 10, domain.User{
|
||||
ID: 20, FirstName: "Target", About: "private about",
|
||||
Birthday: domain.Birthday{Day: 2, Month: 8, Year: 2000},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if privacy.batchCalls != 1 || privacy.scalarCalls != 0 {
|
||||
t.Fatalf("privacy calls batch=%d scalar=%d, want 1/0", privacy.batchCalls, privacy.scalarCalls)
|
||||
}
|
||||
if len(privacy.keys) != 7 {
|
||||
t.Fatalf("batch keys=%v, want seven UserFull privacy keys", privacy.keys)
|
||||
}
|
||||
if full.About != "" || !full.PhoneCallsAvailable || !full.PhoneCallsPrivate || !full.VoiceMessagesForbidden {
|
||||
t.Fatalf("privacy projection=%+v", full)
|
||||
}
|
||||
if _, ok := full.GetBirthday(); !ok {
|
||||
t.Fatal("allowed birthday omitted")
|
||||
}
|
||||
if _, ok := full.GetProfilePhoto(); ok {
|
||||
t.Fatal("missing profile-photo visibility defaulted to visible")
|
||||
}
|
||||
if _, ok := full.GetSavedMusic(); ok {
|
||||
t.Fatal("missing saved-music visibility defaulted to visible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildUserFullProjectionRetainsScalarPrivacyFallback(t *testing.T) {
|
||||
privacy := &userFullScalarPrivacy{visible: map[domain.PrivacyKey]bool{
|
||||
domain.PrivacyKeyAbout: true,
|
||||
domain.PrivacyKeyPhoneCall: true,
|
||||
domain.PrivacyKeyPhoneP2P: true,
|
||||
domain.PrivacyKeyVoiceMessages: true,
|
||||
domain.PrivacyKeyProfilePhoto: true,
|
||||
domain.PrivacyKeySavedMusic: true,
|
||||
domain.PrivacyKeyBirthday: true,
|
||||
}}
|
||||
r := New(Config{}, Deps{Privacy: privacy}, zaptest.NewLogger(t), clock.System)
|
||||
full, err := r.buildUserFullProjection(context.Background(), 10, domain.User{ID: 20, About: "visible"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if privacy.calls != 7 {
|
||||
t.Fatalf("scalar privacy calls=%d, want 7", privacy.calls)
|
||||
}
|
||||
if full.About != "visible" || !full.PhoneCallsAvailable || full.PhoneCallsPrivate || full.VoiceMessagesForbidden {
|
||||
t.Fatalf("scalar privacy projection=%+v", full)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildUserFullProjectionSelfSkipsPrivacyEvaluation(t *testing.T) {
|
||||
privacy := &userFullBatchPrivacy{visible: map[domain.PrivacyKey]bool{}}
|
||||
r := New(Config{}, Deps{Privacy: privacy}, zaptest.NewLogger(t), clock.System)
|
||||
full, err := r.buildUserFullProjection(context.Background(), 20, domain.User{ID: 20, About: "self"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if privacy.batchCalls != 0 || privacy.scalarCalls != 0 || full.About != "self" {
|
||||
t.Fatalf("self projection calls=%d/%d full=%+v", privacy.batchCalls, privacy.scalarCalls, full)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue