feat: add stickers
This commit is contained in:
@@ -2459,3 +2459,153 @@ func TestSend_ReplyCarriesItsQuoteImmediately(t *testing.T) {
|
||||
t.Error("a retried reply lost its quote")
|
||||
}
|
||||
}
|
||||
|
||||
// grantSticker gives a user a reward the way sādhana does, returning its key.
|
||||
func grantSticker(t *testing.T, pg *repository.Postgres, userID, key string) string {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
var rewardID string
|
||||
if err := pg.Pool.QueryRow(ctx, `
|
||||
INSERT INTO sadhna_rewards (key, name, image_url)
|
||||
VALUES ($1, $2, 'https://example.invalid/art.png')
|
||||
ON CONFLICT (key) DO UPDATE SET name = EXCLUDED.name
|
||||
RETURNING id`, key, "Sticker "+key).Scan(&rewardID); err != nil {
|
||||
t.Fatalf("create reward: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pg.Pool.Exec(ctx, `DELETE FROM sadhna_rewards WHERE id = $1`, rewardID)
|
||||
})
|
||||
if userID != "" {
|
||||
// milestone is unique per user; a large random-ish one avoids clashing
|
||||
// with any real streak rows.
|
||||
if _, err := pg.Pool.Exec(ctx, `
|
||||
INSERT INTO sadhna_reward_grants (user_id, reward_id, milestone)
|
||||
VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`,
|
||||
userID, rewardID, 900000+len(key)); err != nil {
|
||||
t.Fatalf("grant reward: %v", err)
|
||||
}
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func stickerAttachment(t *testing.T, msgs []model.Message) *model.Attachment {
|
||||
t.Helper()
|
||||
for i := range msgs {
|
||||
for j := range msgs[i].Attachments {
|
||||
if msgs[i].Attachments[j].Kind == model.AttachmentSticker {
|
||||
return &msgs[i].Attachments[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// A sticker key is not a capability: sending one you never earned must fail,
|
||||
// or the whole catalog is available to anyone who can guess a key.
|
||||
func TestStickers_OnlySendWhatYouOwn(t *testing.T) {
|
||||
svc, _, pg := testChatService(t)
|
||||
ctx := context.Background()
|
||||
a, b := chatUser(t, pg, "A"), chatUser(t, pg, "B")
|
||||
conv, err := svc.OpenDM(ctx, a, b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleanupConv(t, pg, conv.ID)
|
||||
|
||||
mine := grantSticker(t, pg, a, "sticker_owned_"+a[:8])
|
||||
// Exists in the catalog, granted to nobody.
|
||||
unearned := grantSticker(t, pg, "", "sticker_unearned_"+a[:8])
|
||||
|
||||
if _, err := svc.Send(ctx, a, conv.ID, SendRequest{StickerKey: &mine}); err != nil {
|
||||
t.Fatalf("sending an earned sticker: %v", err)
|
||||
}
|
||||
msgs, _ := svc.Messages(ctx, a, conv.ID, 0, 10)
|
||||
att := stickerAttachment(t, msgs)
|
||||
if att == nil {
|
||||
t.Fatal("an earned sticker did not attach")
|
||||
}
|
||||
if att.ImageURL == nil || *att.ImageURL == "" || att.EntityID == nil || *att.EntityID != mine {
|
||||
t.Errorf("attachment = %+v, want the artwork and its key", att)
|
||||
}
|
||||
|
||||
// Not earned, not saved: the message sends, but carries no sticker.
|
||||
if _, err := svc.Send(ctx, a, conv.ID, SendRequest{
|
||||
Body: "trying", StickerKey: &unearned,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
msgs, _ = svc.Messages(ctx, a, conv.ID, 0, 10)
|
||||
for i := range msgs {
|
||||
for j := range msgs[i].Attachments {
|
||||
if att := msgs[i].Attachments[j]; att.Kind == model.AttachmentSticker &&
|
||||
att.EntityID != nil && *att.EntityID == unearned {
|
||||
t.Error("a sticker the sender never earned was attached")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Saving is how a sticker spreads — but only from a message actually sent to
|
||||
// you, in a thread you are actually in.
|
||||
func TestStickers_SaveFromChat(t *testing.T) {
|
||||
svc, _, pg := testChatService(t)
|
||||
ctx := context.Background()
|
||||
a, b, outsider := chatUser(t, pg, "A"), chatUser(t, pg, "B"), chatUser(t, pg, "C")
|
||||
conv, err := svc.OpenDM(ctx, a, b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleanupConv(t, pg, conv.ID)
|
||||
|
||||
key := grantSticker(t, pg, a, "sticker_shared_"+a[:8])
|
||||
sent, err := svc.Send(ctx, a, conv.ID, SendRequest{StickerKey: &key})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// B has not earned it, so it isn't in their drawer yet.
|
||||
before, _ := svc.Stickers(ctx, b)
|
||||
for _, s := range before {
|
||||
if s.Key == key {
|
||||
t.Fatal("B has the sticker before saving it")
|
||||
}
|
||||
}
|
||||
|
||||
if err := svc.SaveSticker(ctx, b, sent.ID, key); err != nil {
|
||||
t.Fatalf("saving a sticker sent to you: %v", err)
|
||||
}
|
||||
after, _ := svc.Stickers(ctx, b)
|
||||
var found *model.Sticker
|
||||
for i := range after {
|
||||
if after[i].Key == key {
|
||||
found = &after[i]
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
t.Fatal("saved sticker is not in B's drawer")
|
||||
}
|
||||
if !found.Saved {
|
||||
t.Error("a sticker from a chat should be marked as saved, not earned")
|
||||
}
|
||||
// B can now send it themselves.
|
||||
if _, err := svc.Send(ctx, b, conv.ID, SendRequest{StickerKey: &key}); err != nil {
|
||||
t.Errorf("B should be able to send a saved sticker: %v", err)
|
||||
}
|
||||
// Saving twice is harmless.
|
||||
if err := svc.SaveSticker(ctx, b, sent.ID, key); err != nil {
|
||||
t.Errorf("second save should be idempotent: %v", err)
|
||||
}
|
||||
|
||||
// Someone outside the thread cannot save from it, even knowing both ids.
|
||||
if err := svc.SaveSticker(ctx, outsider, sent.ID, key); !errors.Is(err, repository.ErrStickerNotOwned) {
|
||||
t.Errorf("outsider save: err = %v, want ErrStickerNotOwned", err)
|
||||
}
|
||||
|
||||
// The sender's own drawer still reports it as earned, not saved.
|
||||
mine, _ := svc.Stickers(ctx, a)
|
||||
for _, s := range mine {
|
||||
if s.Key == key && s.Saved {
|
||||
t.Error("an earned sticker is being reported as saved")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user