chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
523
internal/store/postgres/channel_member_cache_integration_test.go
Normal file
523
internal/store/postgres/channel_member_cache_integration_test.go
Normal file
|
|
@ -0,0 +1,523 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// TestChannelMemberCacheInvalidatesOnReadModelNotify 验证:channel_members 触发器(0120)
|
||||
// → 统一 read-model LISTEN/NOTIFY → ChannelMemberCache 失效 → 下一次读回填新成员态。
|
||||
func TestChannelMemberCacheInvalidatesOnReadModelNotify(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
dsn := os.Getenv("TELESRV_TEST_POSTGRES_DSN")
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
|
||||
users := NewUserStore(pool)
|
||||
owner, err := users.Create(ctx, domain.User{
|
||||
AccessHash: 881,
|
||||
Phone: "+1888" + suffix + "01",
|
||||
FirstName: "MemberCacheOwner",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create owner: %v", err)
|
||||
}
|
||||
|
||||
rowCache := NewChannelRowCache(1000)
|
||||
memberCache := NewChannelMemberCache(1000)
|
||||
channels := NewChannelStore(pool,
|
||||
WithChannelRowCache(rowCache),
|
||||
WithChannelMemberCache(memberCache))
|
||||
|
||||
var channelID int64
|
||||
t.Cleanup(func() {
|
||||
if channelID != 0 {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
|
||||
}
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
|
||||
})
|
||||
|
||||
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: owner.ID,
|
||||
Title: "MemberCacheChan " + suffix,
|
||||
Megagroup: true,
|
||||
Date: 1700001700,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel: %v", err)
|
||||
}
|
||||
channelID = created.Channel.ID
|
||||
|
||||
if _, err := channels.GetChannel(ctx, owner.ID, channelID); err != nil {
|
||||
t.Fatalf("warm GetChannel: %v", err)
|
||||
}
|
||||
if _, ok := rowCache.get(channelID); !ok {
|
||||
t.Fatalf("GetChannel 后频道行应已缓存")
|
||||
}
|
||||
if _, ok := memberCache.get(channelID, owner.ID); !ok {
|
||||
t.Fatalf("GetChannel 后成员行应已缓存")
|
||||
}
|
||||
|
||||
lctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
listener := NewReadModelChangeListener(dsn, ReadModelCacheSet{
|
||||
ChannelRows: rowCache,
|
||||
ChannelMembers: memberCache,
|
||||
}, nil)
|
||||
go listener.Run(lctx)
|
||||
if !waitUntil(2*time.Second, func() bool {
|
||||
_, rowOK := rowCache.get(channelID)
|
||||
_, memberOK := memberCache.get(channelID, owner.ID)
|
||||
return !rowOK && !memberOK
|
||||
}) {
|
||||
t.Fatalf("read model listener 未在预期内连接并 flush 缓存")
|
||||
}
|
||||
|
||||
if view, err := channels.GetChannel(ctx, owner.ID, channelID); err != nil {
|
||||
t.Fatalf("re-warm GetChannel: %v", err)
|
||||
} else if view.Self.Rank != "" {
|
||||
t.Fatalf("初始 rank = %q, want empty", view.Self.Rank)
|
||||
}
|
||||
if _, ok := memberCache.get(channelID, owner.ID); !ok {
|
||||
t.Fatalf("re-warm 后成员行应已缓存")
|
||||
}
|
||||
|
||||
const newRank = "cache-rank"
|
||||
if _, err := pool.Exec(ctx, `
|
||||
UPDATE channel_members
|
||||
SET rank = $3, updated_at = now()
|
||||
WHERE channel_id = $1 AND user_id = $2`, channelID, owner.ID, newRank); err != nil {
|
||||
t.Fatalf("update member rank: %v", err)
|
||||
}
|
||||
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
_, ok := memberCache.get(channelID, owner.ID)
|
||||
return !ok
|
||||
}) {
|
||||
t.Fatalf("UPDATE channel_members 后成员缓存未在预期内被 read-model NOTIFY 失效")
|
||||
}
|
||||
|
||||
view, err := channels.GetChannel(ctx, owner.ID, channelID)
|
||||
if err != nil {
|
||||
t.Fatalf("post-invalidation GetChannel: %v", err)
|
||||
}
|
||||
if view.Self.Rank != newRank {
|
||||
t.Fatalf("失效后 rank = %q, want %q", view.Self.Rank, newRank)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDialogLightInvalidatesOnChannelMemberAndDialogNotify(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
dsn := os.Getenv("TELESRV_TEST_POSTGRES_DSN")
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
|
||||
users := NewUserStore(pool)
|
||||
owner := createTestUser(t, ctx, users, "+1888"+suffix+"91", "DialogLightChannelOwner", "")
|
||||
channels := NewChannelStore(pool)
|
||||
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: owner.ID,
|
||||
Title: "DialogLightChannel " + suffix,
|
||||
Megagroup: true,
|
||||
Date: 1700003300,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel: %v", err)
|
||||
}
|
||||
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: created.Channel.ID}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", created.Channel.ID)
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
|
||||
})
|
||||
|
||||
versionBeforeMember, hashBeforeMember := readDialogLightVersionHash(t, ctx, pool, owner.ID, peer)
|
||||
dialogCache := &fakeDialogReadModelCache{}
|
||||
lctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
listener := NewReadModelChangeListener(dsn, ReadModelCacheSet{Dialogs: dialogCache}, nil)
|
||||
go listener.Run(lctx)
|
||||
if !waitUntil(2*time.Second, func() bool { return dialogCache.flushCount() == 1 }) {
|
||||
t.Fatalf("read model listener 未在预期内连接并 flush dialog cache")
|
||||
}
|
||||
|
||||
if _, err := pool.Exec(ctx, `
|
||||
UPDATE channel_members
|
||||
SET rank = $3, updated_at = now()
|
||||
WHERE channel_id = $1 AND user_id = $2`, created.Channel.ID, owner.ID, "dialog-light-rank"); err != nil {
|
||||
t.Fatalf("update channel member: %v", err)
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
return countDialogInvalidation(dialogCache, owner.ID, peer) > 0
|
||||
}) {
|
||||
owners, peers := dialogCache.entriesSnapshot()
|
||||
t.Fatalf("channel member 写入未触发 dialog_light 失效: owners=%v peers=%+v want owner=%d peer=%+v", owners, peers, owner.ID, peer)
|
||||
}
|
||||
versionAfterMember, hashAfterMember := readDialogLightVersionHash(t, ctx, pool, owner.ID, peer)
|
||||
if versionAfterMember <= versionBeforeMember {
|
||||
t.Fatalf("channel member did not bump dialog_light version: before=%d after=%d", versionBeforeMember, versionAfterMember)
|
||||
}
|
||||
if hashAfterMember == 0 || hashAfterMember == hashBeforeMember {
|
||||
t.Fatalf("channel member did not refresh dialog_light hash: before=%d after=%d", hashBeforeMember, hashAfterMember)
|
||||
}
|
||||
|
||||
beforeDialogInvalidation := countDialogInvalidation(dialogCache, owner.ID, peer)
|
||||
versionBeforeDialog, hashBeforeDialog := readDialogLightVersionHash(t, ctx, pool, owner.ID, peer)
|
||||
changed, err := channels.SetChannelDialogUnreadMark(ctx, owner.ID, created.Channel.ID, true)
|
||||
if err != nil {
|
||||
t.Fatalf("set channel unread mark: %v", err)
|
||||
}
|
||||
if !changed {
|
||||
t.Fatalf("set channel unread mark changed = false, want true")
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
return countDialogInvalidation(dialogCache, owner.ID, peer) > beforeDialogInvalidation
|
||||
}) {
|
||||
owners, peers := dialogCache.entriesSnapshot()
|
||||
t.Fatalf("channel dialog 写入未触发 dialog_light 失效: owners=%v peers=%+v want owner=%d peer=%+v", owners, peers, owner.ID, peer)
|
||||
}
|
||||
versionAfterDialog, hashAfterDialog := readDialogLightVersionHash(t, ctx, pool, owner.ID, peer)
|
||||
if versionAfterDialog <= versionBeforeDialog {
|
||||
t.Fatalf("channel dialog did not bump dialog_light version: before=%d after=%d", versionBeforeDialog, versionAfterDialog)
|
||||
}
|
||||
if hashAfterDialog == 0 || hashAfterDialog == hashBeforeDialog {
|
||||
t.Fatalf("channel dialog did not refresh dialog_light hash: before=%d after=%d", hashBeforeDialog, hashAfterDialog)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadModelChangeListenerInvalidatesProjectionCachesFromNotify 验证 0120/0121
|
||||
// 的 account/profile 投影触发器能穿过统一 listener 分发到对应缓存。
|
||||
func TestReadModelChangeListenerInvalidatesProjectionCachesFromNotify(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
dsn := os.Getenv("TELESRV_TEST_POSTGRES_DSN")
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
|
||||
users := NewUserStore(pool)
|
||||
owner, err := users.Create(ctx, domain.User{
|
||||
AccessHash: 991,
|
||||
Phone: "+1991" + suffix + "01",
|
||||
FirstName: "ProjectionOwner",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create owner: %v", err)
|
||||
}
|
||||
target, err := users.Create(ctx, domain.User{
|
||||
AccessHash: 992,
|
||||
Phone: "+1992" + suffix + "02",
|
||||
FirstName: "ProjectionTarget",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create target: %v", err)
|
||||
}
|
||||
photoID := time.Now().UnixNano()
|
||||
targetPhotoID := photoID + 1
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM account_privacy_rules WHERE owner_user_id = ANY($1::bigint[])", []int64{owner.ID, target.ID})
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM profile_photos WHERE owner_peer_type = 'user' AND owner_peer_id = ANY($1::bigint[])", []int64{owner.ID, target.ID})
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM photos WHERE id = ANY($1::bigint[])", []int64{photoID, targetPhotoID})
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, target.ID})
|
||||
})
|
||||
|
||||
contactsCache := &fakeContactReadModelCache{}
|
||||
dialogCache := &fakeDialogReadModelCache{}
|
||||
privacyCache := &fakePrivacyReadModelCache{}
|
||||
photoCache := &fakeProfilePhotoReadModelCache{}
|
||||
lctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
listener := NewReadModelChangeListener(dsn, ReadModelCacheSet{
|
||||
Contacts: contactsCache,
|
||||
Dialogs: dialogCache,
|
||||
Privacy: privacyCache,
|
||||
ProfilePhotos: photoCache,
|
||||
}, nil)
|
||||
go listener.Run(lctx)
|
||||
if !waitUntil(2*time.Second, func() bool {
|
||||
return contactsCache.flushCount() == 1 && dialogCache.flushCount() == 1 && privacyCache.flushCount() == 1 && photoCache.flushCount() == 1
|
||||
}) {
|
||||
t.Fatalf("read model listener 未在预期内连接并 flush account/dialog/photo caches")
|
||||
}
|
||||
|
||||
dialogs := NewDialogStore(pool)
|
||||
dialogPeer := domain.Peer{Type: domain.PeerTypeUser, ID: target.ID}
|
||||
if err := dialogs.Upsert(ctx, owner.ID, domain.Dialog{Peer: dialogPeer, TopMessage: 1, TopMessageDate: 1700002999}); err != nil {
|
||||
t.Fatalf("upsert dialog: %v", err)
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
return countDialogInvalidation(dialogCache, owner.ID, dialogPeer) > 0
|
||||
}) {
|
||||
owners, peers := dialogCache.entriesSnapshot()
|
||||
t.Fatalf("dialog 写入未触发 dialog_light 失效: owners=%v peers=%+v want owner=%d peer=%+v", owners, peers, owner.ID, dialogPeer)
|
||||
}
|
||||
versionBeforeContactDialog, hashBeforeContactDialog := readDialogLightVersionHash(t, ctx, pool, owner.ID, dialogPeer)
|
||||
|
||||
contacts := NewContactStore(pool)
|
||||
beforeContactDialogFanout := countDialogInvalidation(dialogCache, owner.ID, dialogPeer)
|
||||
if _, err := contacts.Upsert(ctx, owner.ID, domain.ContactInput{ContactUserID: target.ID, FirstName: "Target"}); err != nil {
|
||||
t.Fatalf("upsert contact: %v", err)
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
return containsInt64(contactsCache.idsSnapshot(), owner.ID)
|
||||
}) {
|
||||
t.Fatalf("contacts 写入未触发 contact_account 失效: ids=%v owner=%d", contactsCache.idsSnapshot(), owner.ID)
|
||||
}
|
||||
var versionBeforeProfile, hashBeforeProfile int64
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT version, hash
|
||||
FROM read_model_versions
|
||||
WHERE model = 'contact_account' AND owner_user_id = $1 AND peer_type = 'user' AND peer_id = $1`, owner.ID).Scan(&versionBeforeProfile, &hashBeforeProfile); err != nil {
|
||||
t.Fatalf("read contact_account version/hash before profile update: %v", err)
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
return countDialogInvalidation(dialogCache, owner.ID, dialogPeer) > beforeContactDialogFanout
|
||||
}) {
|
||||
owners, peers := dialogCache.entriesSnapshot()
|
||||
t.Fatalf("contact 写入未 fan-out 到 dialog_light 失效: owners=%v peers=%+v owner=%d peer=%+v", owners, peers, owner.ID, dialogPeer)
|
||||
}
|
||||
versionAfterContactDialog, hashAfterContactDialog := readDialogLightVersionHash(t, ctx, pool, owner.ID, dialogPeer)
|
||||
if versionAfterContactDialog <= versionBeforeContactDialog {
|
||||
t.Fatalf("contact did not bump dialog_light version: before=%d after=%d", versionBeforeContactDialog, versionAfterContactDialog)
|
||||
}
|
||||
if hashAfterContactDialog == 0 || hashAfterContactDialog == hashBeforeContactDialog {
|
||||
t.Fatalf("contact did not refresh dialog_light hash: before=%d after=%d", hashBeforeContactDialog, hashAfterContactDialog)
|
||||
}
|
||||
beforeUserFanout := countInt64(contactsCache.idsSnapshot(), owner.ID)
|
||||
beforeUserDialogFanout := countDialogInvalidation(dialogCache, owner.ID, dialogPeer)
|
||||
versionBeforeProfileDialog, hashBeforeProfileDialog := readDialogLightVersionHash(t, ctx, pool, owner.ID, dialogPeer)
|
||||
if _, err := pool.Exec(ctx, "UPDATE users SET first_name = $2, updated_at = now() WHERE id = $1", target.ID, "ProjectionTargetRenamed"); err != nil {
|
||||
t.Fatalf("update target user profile field: %v", err)
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
return countInt64(contactsCache.idsSnapshot(), owner.ID) > beforeUserFanout
|
||||
}) {
|
||||
t.Fatalf("users profile 写入未 fan-out 到 contact_account 失效: ids=%v owner=%d", contactsCache.idsSnapshot(), owner.ID)
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
return countDialogInvalidation(dialogCache, owner.ID, dialogPeer) > beforeUserDialogFanout
|
||||
}) {
|
||||
owners, peers := dialogCache.entriesSnapshot()
|
||||
t.Fatalf("users profile 写入未 fan-out 到 dialog_light 失效: owners=%v peers=%+v owner=%d peer=%+v", owners, peers, owner.ID, dialogPeer)
|
||||
}
|
||||
versionAfterProfileDialog, hashAfterProfileDialog := readDialogLightVersionHash(t, ctx, pool, owner.ID, dialogPeer)
|
||||
if versionAfterProfileDialog <= versionBeforeProfileDialog {
|
||||
t.Fatalf("profile update did not bump dialog_light version: before=%d after=%d", versionBeforeProfileDialog, versionAfterProfileDialog)
|
||||
}
|
||||
if hashAfterProfileDialog == 0 || hashAfterProfileDialog == hashBeforeProfileDialog {
|
||||
t.Fatalf("profile update did not refresh dialog_light hash: before=%d after=%d", hashBeforeProfileDialog, hashAfterProfileDialog)
|
||||
}
|
||||
var versionBeforeLastSeen, hashBeforeLastSeen int64
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT version, hash
|
||||
FROM read_model_versions
|
||||
WHERE model = 'contact_account' AND owner_user_id = $1 AND peer_type = 'user' AND peer_id = $1`, owner.ID).Scan(&versionBeforeLastSeen, &hashBeforeLastSeen); err != nil {
|
||||
t.Fatalf("read contact_account version before last_seen update: %v", err)
|
||||
}
|
||||
if versionBeforeLastSeen <= versionBeforeProfile {
|
||||
t.Fatalf("profile update did not bump contact_account version: before=%d after=%d", versionBeforeProfile, versionBeforeLastSeen)
|
||||
}
|
||||
if hashBeforeLastSeen == 0 || hashBeforeLastSeen == hashBeforeProfile {
|
||||
t.Fatalf("profile update did not refresh contact_account hash: before=%d after=%d", hashBeforeProfile, hashBeforeLastSeen)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, "UPDATE users SET last_seen_at = GREATEST(last_seen_at, $2), updated_at = now() WHERE id = $1", target.ID, int64(1700003999)); err != nil {
|
||||
t.Fatalf("update target last_seen: %v", err)
|
||||
}
|
||||
var versionAfterLastSeen, hashAfterLastSeen int64
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT version, hash
|
||||
FROM read_model_versions
|
||||
WHERE model = 'contact_account' AND owner_user_id = $1 AND peer_type = 'user' AND peer_id = $1`, owner.ID).Scan(&versionAfterLastSeen, &hashAfterLastSeen); err != nil {
|
||||
t.Fatalf("read contact_account version after last_seen update: %v", err)
|
||||
}
|
||||
if versionAfterLastSeen != versionBeforeLastSeen {
|
||||
t.Fatalf("last_seen update bumped contact_account version: before=%d after=%d", versionBeforeLastSeen, versionAfterLastSeen)
|
||||
}
|
||||
if hashAfterLastSeen != hashBeforeLastSeen {
|
||||
t.Fatalf("last_seen update refreshed contact_account hash: before=%d after=%d", hashBeforeLastSeen, hashAfterLastSeen)
|
||||
}
|
||||
if _, err := contacts.Block(ctx, owner.ID, target.ID, 1700003000); err != nil {
|
||||
t.Fatalf("block contact: %v", err)
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
return countInt64(contactsCache.idsSnapshot(), owner.ID) >= 2
|
||||
}) {
|
||||
t.Fatalf("block 写入未触发 contact_blocklist 失效: ids=%v owner=%d", contactsCache.idsSnapshot(), owner.ID)
|
||||
}
|
||||
|
||||
privacy := NewPrivacyStore(pool)
|
||||
if err := privacy.SetPrivacyRules(ctx, domain.PrivacyRules{
|
||||
OwnerUserID: owner.ID,
|
||||
Key: domain.PrivacyKeyProfilePhoto,
|
||||
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleAllowContacts}},
|
||||
}); err != nil {
|
||||
t.Fatalf("set privacy: %v", err)
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
return containsInt64(privacyCache.idsSnapshot(), owner.ID)
|
||||
}) {
|
||||
t.Fatalf("privacy 写入未触发 privacy_rules 失效: ids=%v owner=%d", privacyCache.idsSnapshot(), owner.ID)
|
||||
}
|
||||
|
||||
media := NewMediaStore(pool)
|
||||
versionBeforeTargetPrivacy, hashBeforeTargetPrivacy := readContactAccountVersionHash(t, ctx, pool, owner.ID)
|
||||
versionBeforeTargetPrivacyDialog, hashBeforeTargetPrivacyDialog := readDialogLightVersionHash(t, ctx, pool, owner.ID, dialogPeer)
|
||||
beforeTargetPrivacyFanout := countInt64(contactsCache.idsSnapshot(), owner.ID)
|
||||
beforeTargetPrivacyDialogFanout := countDialogInvalidation(dialogCache, owner.ID, dialogPeer)
|
||||
if err := privacy.SetPrivacyRules(ctx, domain.PrivacyRules{
|
||||
OwnerUserID: target.ID,
|
||||
Key: domain.PrivacyKeyProfilePhoto,
|
||||
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleDisallowAll}},
|
||||
}); err != nil {
|
||||
t.Fatalf("set target privacy: %v", err)
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
return countInt64(contactsCache.idsSnapshot(), owner.ID) > beforeTargetPrivacyFanout
|
||||
}) {
|
||||
t.Fatalf("target privacy 写入未 fan-out 到 contact_account 失效: ids=%v owner=%d", contactsCache.idsSnapshot(), owner.ID)
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
return countDialogInvalidation(dialogCache, owner.ID, dialogPeer) > beforeTargetPrivacyDialogFanout
|
||||
}) {
|
||||
owners, peers := dialogCache.entriesSnapshot()
|
||||
t.Fatalf("target privacy 写入未 fan-out 到 dialog_light 失效: owners=%v peers=%+v owner=%d peer=%+v", owners, peers, owner.ID, dialogPeer)
|
||||
}
|
||||
versionAfterTargetPrivacy, hashAfterTargetPrivacy := readContactAccountVersionHash(t, ctx, pool, owner.ID)
|
||||
if versionAfterTargetPrivacy <= versionBeforeTargetPrivacy {
|
||||
t.Fatalf("target privacy did not bump contact_account version: before=%d after=%d", versionBeforeTargetPrivacy, versionAfterTargetPrivacy)
|
||||
}
|
||||
if hashAfterTargetPrivacy == 0 || hashAfterTargetPrivacy == hashBeforeTargetPrivacy {
|
||||
t.Fatalf("target privacy did not refresh contact_account hash: before=%d after=%d", hashBeforeTargetPrivacy, hashAfterTargetPrivacy)
|
||||
}
|
||||
versionAfterTargetPrivacyDialog, hashAfterTargetPrivacyDialog := readDialogLightVersionHash(t, ctx, pool, owner.ID, dialogPeer)
|
||||
if versionAfterTargetPrivacyDialog <= versionBeforeTargetPrivacyDialog {
|
||||
t.Fatalf("target privacy did not bump dialog_light version: before=%d after=%d", versionBeforeTargetPrivacyDialog, versionAfterTargetPrivacyDialog)
|
||||
}
|
||||
if hashAfterTargetPrivacyDialog == 0 || hashAfterTargetPrivacyDialog == hashBeforeTargetPrivacyDialog {
|
||||
t.Fatalf("target privacy did not refresh dialog_light hash: before=%d after=%d", hashBeforeTargetPrivacyDialog, hashAfterTargetPrivacyDialog)
|
||||
}
|
||||
|
||||
if err := media.PutPhoto(ctx, domain.Photo{ID: targetPhotoID, AccessHash: 994, FileReference: []byte("target-ref"), Date: 1700003003, DCID: 2}); err != nil {
|
||||
t.Fatalf("put target photo: %v", err)
|
||||
}
|
||||
versionBeforeTargetPhoto, hashBeforeTargetPhoto := readContactAccountVersionHash(t, ctx, pool, owner.ID)
|
||||
versionBeforeTargetPhotoDialog, hashBeforeTargetPhotoDialog := readDialogLightVersionHash(t, ctx, pool, owner.ID, dialogPeer)
|
||||
beforeTargetPhotoFanout := countInt64(contactsCache.idsSnapshot(), owner.ID)
|
||||
beforeTargetPhotoDialogFanout := countDialogInvalidation(dialogCache, owner.ID, dialogPeer)
|
||||
if err := media.AddProfilePhotoKind(ctx, domain.PeerTypeUser, target.ID, domain.ProfilePhotoKindProfile, targetPhotoID, 1700003004); err != nil {
|
||||
t.Fatalf("add target profile photo: %v", err)
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
return countInt64(contactsCache.idsSnapshot(), owner.ID) > beforeTargetPhotoFanout
|
||||
}) {
|
||||
t.Fatalf("target profile photo 写入未 fan-out 到 contact_account 失效: ids=%v owner=%d", contactsCache.idsSnapshot(), owner.ID)
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
return countDialogInvalidation(dialogCache, owner.ID, dialogPeer) > beforeTargetPhotoDialogFanout
|
||||
}) {
|
||||
owners, peers := dialogCache.entriesSnapshot()
|
||||
t.Fatalf("target profile photo 写入未 fan-out 到 dialog_light 失效: owners=%v peers=%+v owner=%d peer=%+v", owners, peers, owner.ID, dialogPeer)
|
||||
}
|
||||
versionAfterTargetPhoto, hashAfterTargetPhoto := readContactAccountVersionHash(t, ctx, pool, owner.ID)
|
||||
if versionAfterTargetPhoto <= versionBeforeTargetPhoto {
|
||||
t.Fatalf("target profile photo did not bump contact_account version: before=%d after=%d", versionBeforeTargetPhoto, versionAfterTargetPhoto)
|
||||
}
|
||||
if hashAfterTargetPhoto == 0 || hashAfterTargetPhoto == hashBeforeTargetPhoto {
|
||||
t.Fatalf("target profile photo did not refresh contact_account hash: before=%d after=%d", hashBeforeTargetPhoto, hashAfterTargetPhoto)
|
||||
}
|
||||
versionAfterTargetPhotoDialog, hashAfterTargetPhotoDialog := readDialogLightVersionHash(t, ctx, pool, owner.ID, dialogPeer)
|
||||
if versionAfterTargetPhotoDialog <= versionBeforeTargetPhotoDialog {
|
||||
t.Fatalf("target profile photo did not bump dialog_light version: before=%d after=%d", versionBeforeTargetPhotoDialog, versionAfterTargetPhotoDialog)
|
||||
}
|
||||
if hashAfterTargetPhotoDialog == 0 || hashAfterTargetPhotoDialog == hashBeforeTargetPhotoDialog {
|
||||
t.Fatalf("target profile photo did not refresh dialog_light hash: before=%d after=%d", hashBeforeTargetPhotoDialog, hashAfterTargetPhotoDialog)
|
||||
}
|
||||
|
||||
versionBeforeDraftDialog, hashBeforeDraftDialog := readDialogLightVersionHash(t, ctx, pool, owner.ID, dialogPeer)
|
||||
beforeDraftDialogFanout := countDialogInvalidation(dialogCache, owner.ID, dialogPeer)
|
||||
if err := dialogs.SaveDraft(ctx, owner.ID, domain.DialogDraft{Peer: dialogPeer, Date: 1700003005, Message: "dialog light draft"}); err != nil {
|
||||
t.Fatalf("save dialog draft: %v", err)
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
return countDialogInvalidation(dialogCache, owner.ID, dialogPeer) > beforeDraftDialogFanout
|
||||
}) {
|
||||
owners, peers := dialogCache.entriesSnapshot()
|
||||
t.Fatalf("dialog draft 写入未触发 dialog_light 失效: owners=%v peers=%+v owner=%d peer=%+v", owners, peers, owner.ID, dialogPeer)
|
||||
}
|
||||
versionAfterDraftDialog, hashAfterDraftDialog := readDialogLightVersionHash(t, ctx, pool, owner.ID, dialogPeer)
|
||||
if versionAfterDraftDialog <= versionBeforeDraftDialog {
|
||||
t.Fatalf("draft did not bump dialog_light version: before=%d after=%d", versionBeforeDraftDialog, versionAfterDraftDialog)
|
||||
}
|
||||
if hashAfterDraftDialog == 0 || hashAfterDraftDialog == hashBeforeDraftDialog {
|
||||
t.Fatalf("draft did not refresh dialog_light hash: before=%d after=%d", hashBeforeDraftDialog, hashAfterDraftDialog)
|
||||
}
|
||||
|
||||
if err := media.PutPhoto(ctx, domain.Photo{ID: photoID, AccessHash: 993, FileReference: []byte("ref"), Date: 1700003001, DCID: 2}); err != nil {
|
||||
t.Fatalf("put photo: %v", err)
|
||||
}
|
||||
if err := media.AddProfilePhotoKind(ctx, domain.PeerTypeUser, owner.ID, domain.ProfilePhotoKindProfile, photoID, 1700003002); err != nil {
|
||||
t.Fatalf("add profile photo: %v", err)
|
||||
}
|
||||
if !waitUntil(3*time.Second, func() bool {
|
||||
for _, ownerPeer := range photoCache.ownersSnapshot() {
|
||||
if ownerPeer == (domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}) {
|
||||
t.Fatalf("profile photo 写入未触发 profile_photo 失效: owners=%+v want user %d", photoCache.ownersSnapshot(), owner.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func readContactAccountVersionHash(t *testing.T, ctx context.Context, pool *pgxpool.Pool, ownerID int64) (int64, int64) {
|
||||
t.Helper()
|
||||
var version, hash int64
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT version, hash
|
||||
FROM read_model_versions
|
||||
WHERE model = 'contact_account' AND owner_user_id = $1 AND peer_type = 'user' AND peer_id = $1`, ownerID).Scan(&version, &hash); err != nil {
|
||||
t.Fatalf("read contact_account version/hash: %v", err)
|
||||
}
|
||||
return version, hash
|
||||
}
|
||||
|
||||
func readDialogLightVersionHash(t *testing.T, ctx context.Context, pool *pgxpool.Pool, ownerID int64, peer domain.Peer) (int64, int64) {
|
||||
t.Helper()
|
||||
var version, hash int64
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT version, hash
|
||||
FROM read_model_versions
|
||||
WHERE model = 'dialog_light' AND owner_user_id = $1 AND peer_type = $2 AND peer_id = $3`, ownerID, peer.Type, peer.ID).Scan(&version, &hash); err != nil {
|
||||
t.Fatalf("read dialog_light version/hash: %v", err)
|
||||
}
|
||||
return version, hash
|
||||
}
|
||||
|
||||
func containsInt64(values []int64, target int64) bool {
|
||||
return countInt64(values, target) > 0
|
||||
}
|
||||
|
||||
func countInt64(values []int64, target int64) int {
|
||||
n := 0
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func countDialogInvalidation(cache *fakeDialogReadModelCache, ownerID int64, peer domain.Peer) int {
|
||||
owners, peers := cache.entriesSnapshot()
|
||||
n := 0
|
||||
for i, owner := range owners {
|
||||
if owner == ownerID && i < len(peers) && peers[i] == peer {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue