package postgres import ( "context" "testing" "telesrv/internal/domain" ) // TestNotifySettingsRoundTripPostgres 回归迁移 0005:per-scope 通知设置真实持久化 // (peer / 类别默认 / 批量 / reset),含可空字段=未设置语义。 func TestNotifySettingsRoundTripPostgres(t *testing.T) { pool := testPool(t) ctx := context.Background() store := NewPasswordStore(pool) users := NewUserStore(pool) suffix := randomSuffix(t) u, err := users.Create(ctx, domain.User{AccessHash: 92, Phone: "+1663" + suffix + "01", FirstName: "NotifyOwner"}) if err != nil { t.Fatalf("create user: %v", err) } t.Cleanup(func() { _, _ = pool.Exec(ctx, "DELETE FROM notify_settings WHERE owner_user_id = $1", u.ID) _, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", u.ID) }) peerA := domain.Peer{Type: domain.PeerTypeUser, ID: 7001} peerB := domain.Peer{Type: domain.PeerTypeChannel, ID: 7002} scopeA := domain.NotifyScope{Kind: domain.NotifyScopePeer, Peer: peerA} // not found. if _, found, err := store.GetNotifySettings(ctx, u.ID, scopeA); err != nil || found { t.Fatalf("get before save = found %v err %v", found, err) } // peer A:只设 mute_until + silent(其余 nil=未设置)。 mute := 2000000000 silent := true if err := store.SaveNotifySettings(ctx, u.ID, scopeA, domain.PeerNotifySettings{MuteUntil: &mute, Silent: &silent}); err != nil { t.Fatalf("save scope A: %v", err) } got, found, err := store.GetNotifySettings(ctx, u.ID, scopeA) if err != nil || !found { t.Fatalf("get scope A = found %v err %v", found, err) } if got.MuteUntil == nil || *got.MuteUntil != mute || got.Silent == nil || !*got.Silent { t.Fatalf("scope A = %+v, want mute=%d silent=true", got, mute) } if got.ShowPreviews != nil || got.StoriesMuted != nil { t.Fatalf("unset fields must stay nil, got %+v", got) } // 类别默认(users)独立。 usersScope := domain.NotifyScope{Kind: domain.NotifyScopeUsers} dmute := 100 if err := store.SaveNotifySettings(ctx, u.ID, usersScope, domain.PeerNotifySettings{MuteUntil: &dmute}); err != nil { t.Fatalf("save users default: %v", err) } if g, found, _ := store.GetNotifySettings(ctx, u.ID, usersScope); !found || g.MuteUntil == nil || *g.MuteUntil != dmute { t.Fatalf("users default = %+v found %v, want mute=%d", g, found, dmute) } // peer A 不受类别默认影响。 if g, _, _ := store.GetNotifySettings(ctx, u.ID, scopeA); g.MuteUntil == nil || *g.MuteUntil != mute { t.Fatalf("scope A polluted by users default: %+v", g) } // upsert 覆盖 + 批量(mute_until 是 TL int32,取接近上限的合法值)。 mute2 := 2100000000 if err := store.SaveNotifySettings(ctx, u.ID, domain.NotifyScope{Kind: domain.NotifyScopePeer, Peer: peerB}, domain.PeerNotifySettings{MuteUntil: &mute2}); err != nil { t.Fatalf("save scope B: %v", err) } batch, err := store.GetPeerNotifySettings(ctx, u.ID, []domain.Peer{peerA, peerB, {Type: domain.PeerTypeUser, ID: 9999}}) if err != nil { t.Fatalf("batch: %v", err) } if len(batch) != 2 { t.Fatalf("batch len = %d, want 2 (peerA+peerB, 9999 unset)", len(batch)) } if a, ok := batch[peerA]; !ok || a.MuteUntil == nil || *a.MuteUntil != mute { t.Fatalf("batch peerA = %+v ok %v", a, ok) } if b, ok := batch[peerB]; !ok || b.MuteUntil == nil || *b.MuteUntil != mute2 { t.Fatalf("batch peerB = %+v ok %v", b, ok) } // AllPeerNotifySettings:返全部整-peer 设置(peerA+peerB),排除类别默认(per-user notify 缓存的加载源)。 all, err := store.AllPeerNotifySettings(ctx, u.ID) if err != nil { t.Fatalf("all peer notify settings: %v", err) } if len(all) != 2 { t.Fatalf("AllPeerNotifySettings = %d, want 2 (peerA+peerB, 类别默认不计)", len(all)) } if a, ok := all[peerA]; !ok || a.MuteUntil == nil || *a.MuteUntil != mute { t.Fatalf("AllPeerNotifySettings peerA = %+v ok %v", a, ok) } // ListNotifyExceptions:只返 per-peer 非默认(peerA+peerB),排除类别默认。 exceptions, err := store.ListNotifyExceptions(ctx, u.ID) if err != nil { t.Fatalf("list exceptions: %v", err) } if len(exceptions) != 2 { t.Fatalf("exceptions = %d, want 2 (peerA+peerB, 类别默认不计)", len(exceptions)) } seen := map[domain.Peer]bool{} for _, ex := range exceptions { seen[ex.Peer] = true } if !seen[peerA] || !seen[peerB] { t.Fatalf("exceptions peers = %v, want peerA+peerB", seen) } // reset 清空全部作用域。 if err := store.ResetNotifySettings(ctx, u.ID); err != nil { t.Fatalf("reset: %v", err) } if ex, _ := store.ListNotifyExceptions(ctx, u.ID); len(ex) != 0 { t.Fatalf("exceptions after reset = %d, want 0", len(ex)) } if _, found, _ := store.GetNotifySettings(ctx, u.ID, scopeA); found { t.Fatalf("scope A must be gone after reset") } if _, found, _ := store.GetNotifySettings(ctx, u.ID, usersScope); found { t.Fatalf("users default must be gone after reset") } }