owpengram-server/internal/rpc/music_rpc_test.go

309 lines
11 KiB
Go

package rpc
import (
"context"
"strings"
"testing"
"github.com/iamxvbaba/td/bin"
"github.com/iamxvbaba/td/clock"
"github.com/iamxvbaba/td/tg"
"go.uber.org/zap/zaptest"
appaccount "telesrv/internal/app/account"
appmessages "telesrv/internal/app/messages"
appusers "telesrv/internal/app/users"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
func TestSavedMusicRPCLifecycle(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, err := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550000011", FirstName: "Owner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
passwords := memory.NewPasswordStore()
music1 := testMusicDocument(101, 1001, "First")
music2 := testMusicDocument(102, 1002, "Second")
voice := domain.Document{ID: 103, AccessHash: 1003, MimeType: "audio/ogg", Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrAudio, Voice: true, AudioDuration: 3}}}
files := &fakeFiles{docs: map[int64]domain.Document{
music1.ID: music1,
music2.ID: music2,
voice.ID: voice,
}}
r := New(Config{}, Deps{
Account: appaccount.NewService(passwords, appaccount.WithSavedMusic(passwords)),
Files: files,
Users: appusers.NewService(userStore),
}, zaptest.NewLogger(t), clock.System)
reqCtx := WithUserID(ctx, owner.ID)
if ok, err := r.onAccountSaveMusic(reqCtx, &tg.AccountSaveMusicRequest{
ID: &tg.InputDocument{ID: music1.ID, AccessHash: music1.AccessHash},
}); err != nil || !ok {
t.Fatalf("save first music = ok %v err %v, want true/nil", ok, err)
}
afterFirst := &tg.AccountSaveMusicRequest{
ID: &tg.InputDocument{ID: music2.ID, AccessHash: music2.AccessHash},
}
afterFirst.SetAfterID(&tg.InputDocument{ID: music1.ID, AccessHash: music1.AccessHash})
if ok, err := r.onAccountSaveMusic(reqCtx, afterFirst); err != nil || !ok {
t.Fatalf("save second after first = ok %v err %v, want true/nil", ok, err)
}
idsBox, err := r.onAccountGetSavedMusicIDs(reqCtx, 0)
if err != nil {
t.Fatalf("get saved music ids: %v", err)
}
ids, ok := idsBox.(*tg.AccountSavedMusicIDs)
if !ok || !int64SliceEqual(ids.IDs, []int64{music1.ID, music2.ID}) {
t.Fatalf("ids = %T %+v, want [first second]", idsBox, idsBox)
}
if notModified, err := r.onAccountGetSavedMusicIDs(reqCtx, int64(tdesktopCountHash(ids.IDs))); err != nil {
t.Fatalf("get saved music ids hash: %v", err)
} else if _, ok := notModified.(*tg.AccountSavedMusicIDsNotModified); !ok {
t.Fatalf("hash result = %T, want notModified", notModified)
}
page, err := r.onUsersGetSavedMusic(reqCtx, &tg.UsersGetSavedMusicRequest{ID: &tg.InputUserSelf{}, Limit: 2})
if err != nil {
t.Fatalf("users.getSavedMusic: %v", err)
}
saved, ok := page.(*tg.UsersSavedMusic)
if !ok || saved.Count != 2 || len(saved.Documents) != 2 || documentClassID(saved.Documents[0]) != music1.ID {
t.Fatalf("saved music page = %T %+v, want two docs with first first", page, page)
}
hashPage, err := r.onUsersGetSavedMusic(reqCtx, &tg.UsersGetSavedMusicRequest{
ID: &tg.InputUserSelf{},
Limit: 2,
Hash: int64(tdesktopCountHash([]int64{music1.ID, music2.ID})),
})
if err != nil {
t.Fatalf("users.getSavedMusic hash: %v", err)
}
if nm, ok := hashPage.(*tg.UsersSavedMusicNotModified); !ok || nm.Count != 2 {
t.Fatalf("hash page = %T %+v, want notModified count=2", hashPage, hashPage)
}
full, err := r.onUsersGetFullUser(reqCtx, &tg.InputUserSelf{})
if err != nil {
t.Fatalf("users.getFullUser: %v", err)
}
if doc, ok := full.FullUser.GetSavedMusicAsNotEmpty(); !ok || doc.ID != music1.ID {
t.Fatalf("full saved_music = %+v ok=%v, want first document", doc, ok)
}
byID, err := r.onUsersGetSavedMusicByID(reqCtx, &tg.UsersGetSavedMusicByIDRequest{
ID: &tg.InputUserSelf{},
Documents: []tg.InputDocumentClass{&tg.InputDocument{ID: music2.ID, AccessHash: music2.AccessHash}},
})
if err != nil {
t.Fatalf("users.getSavedMusicByID: %v", err)
}
byIDMusic, ok := byID.(*tg.UsersSavedMusic)
if !ok || len(byIDMusic.Documents) != 1 || documentClassID(byIDMusic.Documents[0]) != music2.ID {
t.Fatalf("by id = %T %+v, want second document", byID, byID)
}
if ok, err := r.onAccountSaveMusic(reqCtx, &tg.AccountSaveMusicRequest{
ID: &tg.InputDocument{ID: music2.ID, AccessHash: music2.AccessHash},
}); err != nil || !ok {
t.Fatalf("move second to top = ok %v err %v, want true/nil", ok, err)
}
idsBox, err = r.onAccountGetSavedMusicIDs(reqCtx, 0)
if err != nil {
t.Fatalf("get reordered ids: %v", err)
}
ids = idsBox.(*tg.AccountSavedMusicIDs)
if !int64SliceEqual(ids.IDs, []int64{music2.ID, music1.ID}) {
t.Fatalf("reordered ids = %v, want [second first]", ids.IDs)
}
unsave := &tg.AccountSaveMusicRequest{ID: &tg.InputDocument{ID: music2.ID, AccessHash: music2.AccessHash}}
unsave.SetUnsave(true)
if ok, err := r.onAccountSaveMusic(reqCtx, unsave); err != nil || !ok {
t.Fatalf("unsave second = ok %v err %v, want true/nil", ok, err)
}
idsBox, err = r.onAccountGetSavedMusicIDs(reqCtx, 0)
if err != nil {
t.Fatalf("get ids after unsave: %v", err)
}
ids = idsBox.(*tg.AccountSavedMusicIDs)
if !int64SliceEqual(ids.IDs, []int64{music1.ID}) {
t.Fatalf("ids after unsave = %v, want [first]", ids.IDs)
}
if _, err := r.onAccountSaveMusic(reqCtx, &tg.AccountSaveMusicRequest{
ID: &tg.InputDocument{ID: voice.ID, AccessHash: voice.AccessHash},
}); err == nil || !strings.Contains(err.Error(), "DOCUMENT_INVALID") {
t.Fatalf("save voice err = %v, want DOCUMENT_INVALID", err)
}
}
func TestMessagesSearchMusicFiltersAudioDocuments(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
alice, err := userStore.Create(ctx, domain.User{AccessHash: 21, Phone: "15550000021", FirstName: "Alice"})
if err != nil {
t.Fatalf("create alice: %v", err)
}
bob, err := userStore.Create(ctx, domain.User{AccessHash: 22, Phone: "15550000022", FirstName: "Bob"})
if err != nil {
t.Fatalf("create bob: %v", err)
}
dialogs := memory.NewDialogStore()
messageStore := memory.NewMessageStore(dialogs)
music := testMusicDocument(201, 2001, "Song")
voice := domain.Document{ID: 202, AccessHash: 2002, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrAudio, Voice: true, AudioDuration: 5}}}
plain := domain.Document{ID: 203, AccessHash: 2003, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrFilename, FileName: "file.bin"}}}
for i, media := range []*domain.MessageMedia{
{Kind: domain.MessageMediaKindDocument, Document: &plain},
{Kind: domain.MessageMediaKindDocument, Document: &voice, Voice: true},
{Kind: domain.MessageMediaKindDocument, Document: &music},
} {
if _, err := messageStore.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: alice.ID,
RecipientUserID: bob.ID,
RandomID: int64(300 + i),
Message: "",
Media: media,
Date: 1700000000 + i,
}); err != nil {
t.Fatalf("send media %d: %v", i, err)
}
}
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Messages: appmessages.NewService(messageStore, dialogs),
}, zaptest.NewLogger(t), clock.System)
var payload bin.Buffer
if err := (&tg.MessagesSearchRequest{
Peer: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash},
Filter: &tg.InputMessagesFilterMusic{},
Limit: 10,
}).Encode(&payload); err != nil {
t.Fatalf("encode messages.search: %v", err)
}
got, err := r.Dispatch(WithUserID(ctx, bob.ID), [8]byte{}, 0, &payload)
if err != nil {
t.Fatalf("messages.search music: %v", err)
}
messages, _, _ := searchMessagesPayload(t, got)
if len(messages) != 1 {
t.Fatalf("music search messages = %d, want 1", len(messages))
}
msg, ok := messages[0].(*tg.Message)
if !ok {
t.Fatalf("music search message = %T, want *tg.Message", messages[0])
}
docMedia, ok := msg.Media.(*tg.MessageMediaDocument)
if !ok || documentClassID(docMedia.Document) != music.ID {
t.Fatalf("music search media = %T %+v, want music document", msg.Media, msg.Media)
}
}
func TestMessagesSearchGlobalMusicAllowsEmptyQuery(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
alice, err := userStore.Create(ctx, domain.User{AccessHash: 31, Phone: "15550000031", FirstName: "Alice"})
if err != nil {
t.Fatalf("create alice: %v", err)
}
bob, err := userStore.Create(ctx, domain.User{AccessHash: 32, Phone: "15550000032", FirstName: "Bob"})
if err != nil {
t.Fatalf("create bob: %v", err)
}
dialogs := memory.NewDialogStore()
messageStore := memory.NewMessageStore(dialogs)
music := testMusicDocument(301, 3001, "Global Song")
voice := domain.Document{ID: 302, AccessHash: 3002, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrAudio, Voice: true, AudioDuration: 5}}}
for i, media := range []*domain.MessageMedia{
{Kind: domain.MessageMediaKindDocument, Document: &voice, Voice: true},
{Kind: domain.MessageMediaKindDocument, Document: &music},
} {
if _, err := messageStore.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: alice.ID,
RecipientUserID: bob.ID,
RandomID: int64(400 + i),
Media: media,
Date: 1700000100 + i,
}); err != nil {
t.Fatalf("send media %d: %v", i, err)
}
}
r := New(Config{}, Deps{
Users: appusers.NewService(userStore),
Messages: appmessages.NewService(messageStore, dialogs),
}, zaptest.NewLogger(t), clock.System)
reqCtx := WithUserID(ctx, bob.ID)
if _, err := r.onMessagesSearchGlobal(reqCtx, &tg.MessagesSearchGlobalRequest{
Filter: &tg.InputMessagesFilterEmpty{},
OffsetPeer: &tg.InputPeerEmpty{},
Limit: 10,
}); err == nil || !strings.Contains(err.Error(), "SEARCH_QUERY_EMPTY") {
t.Fatalf("empty non-music searchGlobal err = %v, want SEARCH_QUERY_EMPTY", err)
}
got, err := r.onMessagesSearchGlobal(reqCtx, &tg.MessagesSearchGlobalRequest{
Filter: &tg.InputMessagesFilterMusic{},
OffsetPeer: &tg.InputPeerEmpty{},
Limit: 10,
})
if err != nil {
t.Fatalf("empty music searchGlobal: %v", err)
}
messages := messagesFromMessagesClass(t, got)
if len(messages) != 1 {
t.Fatalf("global music messages = %d, want 1", len(messages))
}
msg, ok := messages[0].(*tg.Message)
if !ok {
t.Fatalf("global music message = %T, want *tg.Message", messages[0])
}
docMedia, ok := msg.Media.(*tg.MessageMediaDocument)
if !ok || documentClassID(docMedia.Document) != music.ID {
t.Fatalf("global music media = %T %+v, want music document", msg.Media, msg.Media)
}
}
func testMusicDocument(id, accessHash int64, title string) domain.Document {
return domain.Document{
ID: id,
AccessHash: accessHash,
MimeType: "audio/mpeg",
Attributes: []domain.DocumentAttribute{
{Kind: domain.DocAttrAudio, AudioDuration: 180, Title: title, Performer: "Artist"},
},
}
}
func documentClassID(doc tg.DocumentClass) int64 {
if d, ok := doc.(*tg.Document); ok {
return d.ID
}
return 0
}
func int64SliceEqual(a, b []int64) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func messagesFromMessagesClass(t *testing.T, got tg.MessagesMessagesClass) []tg.MessageClass {
t.Helper()
switch v := got.(type) {
case *tg.MessagesMessages:
return v.Messages
case *tg.MessagesMessagesSlice:
return v.Messages
default:
t.Fatalf("messages class = %T, want messages/messagesSlice", got)
return nil
}
}