261 lines
7.3 KiB
Go
261 lines
7.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/joho/godotenv"
|
|
"github.com/prabhupadasays/spsays-bff/internal/repository"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func testRewardRepo(t *testing.T) (*repository.RewardRepo, *repository.Postgres) {
|
|
t.Helper()
|
|
uri := os.Getenv("POSTGRES_URI")
|
|
if uri == "" {
|
|
_ = godotenv.Load(filepath.Join("..", "..", ".env"))
|
|
uri = os.Getenv("POSTGRES_URI")
|
|
}
|
|
if uri == "" {
|
|
t.Skip("POSTGRES_URI not set")
|
|
}
|
|
pg, err := repository.NewPostgres(context.Background(), uri, zap.NewNop())
|
|
if err != nil {
|
|
t.Skipf("postgres unreachable: %v", err)
|
|
}
|
|
t.Cleanup(pg.Close)
|
|
return repository.NewRewardRepo(pg), pg
|
|
}
|
|
|
|
func giftUser(t *testing.T, pg *repository.Postgres) string {
|
|
t.Helper()
|
|
id := uuid.NewString()
|
|
suffix := id[:8]
|
|
if _, err := pg.Pool.Exec(context.Background(), `
|
|
INSERT INTO users (id, email, password_hash, name, handle)
|
|
VALUES ($1, $2, 'x', 'Gift User', $3)`,
|
|
id, "gift-"+suffix+"@example.invalid", "gift_"+suffix); err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_, _ = pg.Pool.Exec(context.Background(), `DELETE FROM users WHERE id = $1`, id)
|
|
})
|
|
return id
|
|
}
|
|
|
|
func giftReward(t *testing.T, pg *repository.Postgres) string {
|
|
t.Helper()
|
|
key := "gift_" + uuid.NewString()[:8]
|
|
if _, err := pg.Pool.Exec(context.Background(), `
|
|
INSERT INTO sadhna_rewards (key, name, image_url)
|
|
VALUES ($1, 'Occasional Gift', 'https://example.invalid/g.png')`, key); err != nil {
|
|
t.Fatalf("create reward: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_, _ = pg.Pool.Exec(context.Background(), `DELETE FROM sadhna_rewards WHERE key = $1`, key)
|
|
})
|
|
return key
|
|
}
|
|
|
|
// A gift arrives wrapped, opens once, and lands in the vault — and re-gifting
|
|
// must not duplicate it.
|
|
func TestGift_ArrivesWrappedAndOpensOnce(t *testing.T) {
|
|
rewards, pg := testRewardRepo(t)
|
|
ctx := context.Background()
|
|
user := giftUser(t, pg)
|
|
key := giftReward(t, pg)
|
|
|
|
reward, err := rewards.ByKey(ctx, key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := rewards.GrantGiftToEveryone(ctx, reward.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
find := func() (found bool, opened bool) {
|
|
grants, err := rewards.GrantsByUser(ctx, user)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, g := range grants {
|
|
if g.Reward.Key == key {
|
|
if g.Source != "gift" {
|
|
t.Errorf("source = %q, want gift", g.Source)
|
|
}
|
|
if g.Milestone != nil {
|
|
t.Errorf("milestone = %v, want nil — a gift isn't a streak day", *g.Milestone)
|
|
}
|
|
return true, g.Opened
|
|
}
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
found, opened := find()
|
|
if !found {
|
|
t.Fatal("the gift did not reach an existing account")
|
|
}
|
|
if opened {
|
|
t.Error("a new gift should arrive wrapped")
|
|
}
|
|
|
|
// It must NOT hijack the streak confetti — that stays for streak rewards.
|
|
if pending, _ := rewards.PendingGrant(ctx, user); pending != nil {
|
|
t.Errorf("a gift appeared as a pending streak celebration: %+v", pending)
|
|
}
|
|
|
|
if err := rewards.OpenGift(ctx, user, key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, opened = find(); !opened {
|
|
t.Error("opening the gift did not un-blur it")
|
|
}
|
|
// Opening twice is harmless.
|
|
if err := rewards.OpenGift(ctx, user, key); err != nil {
|
|
t.Errorf("second open should be a no-op: %v", err)
|
|
}
|
|
|
|
// Re-gifting tops up newcomers without duplicating anyone's tile.
|
|
if _, err := rewards.GrantGiftToEveryone(ctx, reward.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
grants, _ := rewards.GrantsByUser(ctx, user)
|
|
n := 0
|
|
for _, g := range grants {
|
|
if g.Reward.Key == key {
|
|
n++
|
|
}
|
|
}
|
|
if n != 1 {
|
|
t.Errorf("holdings of the gift = %d, want 1", n)
|
|
}
|
|
// ...and it must not silently re-wrap what was already opened.
|
|
if _, opened = find(); !opened {
|
|
t.Error("re-gifting re-wrapped an already opened gift")
|
|
}
|
|
}
|
|
|
|
// Deleting a custom gift must clear it from every gallery it reached, and must
|
|
// refuse to touch the seeded catalog — a seeded reward would return on the
|
|
// next Seed run, after having been ripped out of people's galleries.
|
|
func TestGift_DeleteRemovesItFromEveryGallery(t *testing.T) {
|
|
rewards, pg := testRewardRepo(t)
|
|
ctx := context.Background()
|
|
user := giftUser(t, pg)
|
|
|
|
// A custom reward, as the admin panel creates them.
|
|
key := "custom_probe_" + uuid.NewString()[:8]
|
|
if _, err := pg.Pool.Exec(ctx, `
|
|
INSERT INTO sadhna_rewards (key, name, image_url)
|
|
VALUES ($1, 'Probe Gift', 'https://example.invalid/p.png')`, key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reward, err := rewards.ByKey(ctx, key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := rewards.GrantGiftToEveryone(ctx, reward.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
held := func() int {
|
|
grants, err := rewards.GrantsByUser(ctx, user)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
n := 0
|
|
for _, g := range grants {
|
|
if g.Reward.Key == key {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
if held() != 1 {
|
|
t.Fatalf("setup: the gift did not reach the gallery")
|
|
}
|
|
|
|
if err := rewards.DeleteCustom(ctx, key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if held() != 0 {
|
|
t.Error("deleting the reward left it in a devotee's gallery")
|
|
}
|
|
if _, err := rewards.ByKey(ctx, key); !errors.Is(err, repository.ErrRewardNotFound) {
|
|
t.Errorf("reward still in the catalog: %v", err)
|
|
}
|
|
|
|
// Seeded rewards are off limits.
|
|
var seeded string
|
|
if err := pg.Pool.QueryRow(ctx,
|
|
`SELECT key FROM sadhna_rewards WHERE key NOT LIKE 'custom\_%' LIMIT 1`,
|
|
).Scan(&seeded); err != nil {
|
|
t.Skip("no seeded rewards to test against")
|
|
}
|
|
if err := rewards.DeleteCustom(ctx, seeded); !errors.Is(err, repository.ErrRewardNotFound) {
|
|
t.Errorf("a seeded reward was deletable: %v", err)
|
|
}
|
|
if _, err := rewards.ByKey(ctx, seeded); err != nil {
|
|
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")
|
|
}
|
|
}
|
|
}
|