fix: hide suspended account

This commit is contained in:
Krispire Admin
2026-08-31 12:28:25 +05:30
parent ac92bc2cf3
commit 44fd1d69ab
8 changed files with 345 additions and 9 deletions
+54
View File
@@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"testing"
"time"
"github.com/google/uuid"
"github.com/joho/godotenv"
@@ -204,3 +205,56 @@ func TestGift_DeleteRemovesItFromEveryGallery(t *testing.T) {
t.Errorf("the seeded reward was removed: %v", err)
}
}
// A suspended account must not be discoverable. One query backs the people
// search page, the mention autocomplete and the chat member pickers, so a leak
// here surfaces in all three.
func TestBannedUsers_AreNotDiscoverable(t *testing.T) {
_, pg := testRewardRepo(t)
ctx := context.Background()
users := repository.NewUserRepo(pg)
// A distinctive name so the search can't match anyone else.
marker := "Zzbanprobe" + uuid.NewString()[:6]
active := giftUser(t, pg)
banned := giftUser(t, pg)
for id, name := range map[string]string{active: marker + " Active", banned: marker + " Banned"} {
if _, err := pg.Pool.Exec(ctx, `UPDATE users SET name = $2 WHERE id = $1`, id, name); err != nil {
t.Fatal(err)
}
}
if _, err := pg.Pool.Exec(ctx,
`UPDATE users SET banned_at = now() WHERE id = $1`, banned); err != nil {
t.Fatal(err)
}
found, total, err := users.Search(ctx, marker, 20, 0)
if err != nil {
t.Fatal(err)
}
ids := map[string]bool{}
for _, u := range found {
ids[u.ID] = true
}
if !ids[active] {
t.Error("the active account is missing from search")
}
if ids[banned] {
t.Error("a suspended account is searchable — it can be @mentioned or added to a group")
}
// The count backs pagination, so it has to agree with the page.
if total != len(found) {
t.Errorf("total = %d but page returned %d — count and page WHERE disagree", total, len(found))
}
// And the welcome strip, which is the most prominent placement there is.
recent, err := users.RecentlyJoined(ctx, active, time.Now().Add(-24*time.Hour), 20)
if err != nil {
t.Fatal(err)
}
for _, u := range recent {
if u.ID == banned {
t.Error("a suspended account appears in the new-users strip")
}
}
}
+56
View File
@@ -6,6 +6,7 @@ package service
import (
"context"
"errors"
"fmt"
"math/rand"
"regexp"
@@ -31,6 +32,9 @@ func NewVaniService(repo *repository.VaniRepo, logger *zap.Logger) *VaniService
// Today returns the current day's pick, choosing (and persisting) one on the
// first call of the day. Returns (nil, nil) only if there's genuinely no
// content to pick.
// ErrVaniBadKind is a kind outside verse/prose/transcript.
var ErrVaniBadKind = errors.New("kind must be verse, prose or transcript")
func (s *VaniService) Today(ctx context.Context) (*model.VaniPayload, error) {
now := time.Now().UTC()
day := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
@@ -75,6 +79,58 @@ func (s *VaniService) Today(ctx context.Context) (*model.VaniPayload, error) {
// choose tries the three content kinds in random order and returns the first
// successful sample, so each day's pick varies across content types.
// SetForDay pins a specific piece of content as a given day's Vāṇī.
//
// Takes only a kind and a content id — never a payload. The display blob is
// rebuilt here with the same builders the random picker uses, so a pinned pick
// is indistinguishable from an automatic one and a client cannot dictate what
// the home page says.
func (s *VaniService) SetForDay(ctx context.Context, day time.Time, kind, hex string) (*model.VaniPick, error) {
day = day.UTC().Truncate(24 * time.Hour)
var pick *model.VaniPick
switch kind {
case "verse":
v, err := s.repo.VerseByHex(ctx, hex)
if err != nil {
return nil, err
}
pick = versePick(v, day)
case "prose":
p, err := s.repo.ProseByHex(ctx, hex)
if err != nil {
return nil, err
}
pick = prosePick(p, s.repo.BookTitle(ctx, p.Book), day)
case "transcript":
t, err := s.repo.TranscriptByHex(ctx, hex)
if err != nil {
return nil, err
}
pick = transcriptPick(t, day)
default:
return nil, ErrVaniBadKind
}
if err := s.repo.Upsert(ctx, pick); err != nil {
return nil, err
}
return pick, nil
}
// ClearForDay drops a pinned pick, handing the day back to the random picker.
func (s *VaniService) ClearForDay(ctx context.Context, day time.Time) error {
return s.repo.Clear(ctx, day.UTC().Truncate(24*time.Hour))
}
// Schedule lists decided days from today onward, for the admin view.
func (s *VaniService) Schedule(ctx context.Context, limit int) ([]model.VaniPick, error) {
if limit <= 0 || limit > 60 {
limit = 30
}
return s.repo.PicksFrom(ctx, time.Now().UTC().Truncate(24*time.Hour), limit)
}
func (s *VaniService) choose(ctx context.Context, exclude map[string][]string, day time.Time) *model.VaniPick {
kinds := []string{"verse", "prose", "transcript"}
rand.Shuffle(len(kinds), func(i, j int) { kinds[i], kinds[j] = kinds[j], kinds[i] })