package products import ( "bytes" "encoding/json" "fmt" "strings" "sync" "testing" "git.t5de.dev/t5de/t5de-proxy/internal/hub" "git.t5de.dev/t5de/t5de-proxy/internal/imq" "git.t5de.dev/t5de/t5de-proxy/internal/proxy" "git.t5de.dev/t5de/t5de-proxy/internal/xmlrpc" ) // fakeObserver records what the hub client would be told. type fakeObserver struct { mu sync.Mutex events []string } func (f *fakeObserver) add(e string) { f.mu.Lock() defer f.mu.Unlock() f.events = append(f.events, e) } func (f *fakeObserver) Session(int64) {} func (f *fakeObserver) Outfit(int64, []hub.Item) {} func (f *fakeObserver) Join(chat, room string) { f.add("join " + chat + " " + room) } func (f *fakeObserver) Leave(chat string) { f.add("leave " + chat) } // delivered reads the chat lines delivered to a client: "from: text". func delivered(t *testing.T, b *bytes.Buffer) []string { t.Helper() var lines []string for b.Len() > 0 { f, n, err := imq.Next(b.Bytes()) if err != nil || n == 0 { t.Fatalf("bad frames: %v", err) } b.Next(n) m, err := imq.ParseDelivered(f.Data) if err != nil || m.Queue != "/chat/555" || m.Mount != "messages" { t.Fatalf("delivered %+v %v", m, err) } var msg struct { Message string UserID int64 `json:"userId"` } json.Unmarshal(m.Message, &msg) lines = append(lines, fmt.Sprintf("%s(%d): %s", m.UserID, msg.UserID, msg.Message)) } return lines } func TestRoomChannel(t *testing.T) { h, _ := newHarness(t) obs := &fakeObserver{} h.p.Observer = obs local := h.p.byProductID("blue-coat").local // Joining a room: getOrMakeChat says which chat it is, then the client subscribes to its queue. h.do("POST", "http://chat.imvu.com/api/xmlrpc/chat.php", call("chat.getOrMakeChat", xmlrpc.Struct{{Name: "userId", Value: int64(7)}, {Name: "activity", Value: "publicroom-12-3"}}), false, false) var toClient bytes.Buffer c := proxy.NewIMQConn(1, &toClient) h.p.Subscribed(c, []string{"/user/7", "/chat/555"}) if got := strings.Join(obs.events, "; "); got != "join 555 12-3" { t.Fatalf("observer heard %q", got) } // Someone else wears the coat (and something not in the packs): the client is given it from them. h.p.Incoming(c, imq.Delivered{UserID: "/user/88", Queue: "/chat/555", Mount: "messages", Message: []byte(`{"message":"hi","userId":88}`)}) h.p.Others("555", []hub.Avatar{{IMVUID: 88, Items: []hub.Item{{ID: "blue-coat", Name: "Coat"}, {ID: "elsewhere", Name: "Hat"}}}}) if got := delivered(t, &toClient); len(got) != 1 || got[0] != fmt.Sprintf("/user/88(88): *use %d", local) { t.Fatalf("delivered %q", got) } // Told again with no change: nothing new. h.p.Others("555", []hub.Avatar{{IMVUID: 88, Items: []hub.Item{{ID: "blue-coat", Name: "Coat"}}}}) if toClient.Len() != 0 { t.Fatal("delivered again") } // Their whole outfit (which IMVU sends without it) gets it added; other lines don't. whole := []byte(`{"message":"*use 80 1 2","userId":88,"chatId":555,"to":0}`) out := h.p.Incoming(c, imq.Delivered{UserID: "/user/88", Queue: "/chat/555", Mount: "messages", Message: whole}) if !bytes.Contains(out, []byte(fmt.Sprintf(`"message":"*use 80 1 2 %d"`, local))) || !bytes.Contains(out, []byte(`"chatId":555`)) { t.Fatalf("whole outfit %s", out) } part := []byte(`{"message":"*use 3","userId":88}`) if out := h.p.Incoming(c, imq.Delivered{UserID: "/user/88", Queue: "/chat/555", Mount: "messages", Message: part}); !bytes.Equal(out, part) { t.Fatalf("a single product changed: %s", out) } other := []byte(`{"message":"*use 80 1","userId":99}`) if out := h.p.Incoming(c, imq.Delivered{UserID: "/user/99", Queue: "/chat/555", Mount: "messages", Message: other}); !bytes.Equal(out, other) { t.Fatalf("someone else's outfit changed: %s", out) } // They take it off, then leave: the client is told to take it off once. h.p.Others("555", []hub.Avatar{{IMVUID: 88}}) h.p.Others("555", []hub.Avatar{}) if got := delivered(t, &toClient); len(got) != 1 || got[0] != fmt.Sprintf("/user/88(88): *remove %d", local) { t.Fatalf("delivered %q", got) } // The connection closing leaves the chat. h.p.Closed(c) if got := obs.events[len(obs.events)-1]; got != "leave 555" { t.Fatalf("last event %q", got) } } func TestParticipantPostKeepsCommunityProducts(t *testing.T) { h, _ := newHarness(t) local := h.p.byProductID("blue-coat").local h.do("POST", "https://secure.imvu.com/api/xmlrpc/client.php", call("test.avatarInfoForLogin2"), false, false) body := []byte(fmt.Sprintf(`{"legacy_outfit_message": "*use 80 1 %d", "seat_number": 2}`, local)) if status, _ := h.do("POST", "https://api.imvu.com/chat/chat-555/participants/user-7", body, true, false); status != 200 { t.Fatalf("status %d", status) } got := h.imvu.asked["participant"] if len(got) != 1 || strings.Contains(got[0], fmt.Sprint(local)) || !strings.Contains(got[0], `"legacy_outfit_message":"*use 80 1"`) || !strings.Contains(got[0], `"seat_number":2`) { t.Fatalf("IMVU was sent %q", got) } }