package service import ( "context" "errors" "os" "path/filepath" "testing" "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) } }