CI / build (push) Successful in 28s
The proxy follows the client's IMQ chat subscriptions and tells the hub which chats it's in (and the room, from chat.getOrMakeChat). For the other T5DE users the hub says are in a chat, it gives the client the community products it has packs for, as *use and *remove lines from them, and adds them to the whole outfits they send. It also takes community products out of the outfit the client posts to IMVU's chat participant endpoint, which had been sending them to IMVU. Co-Authored-By: Claude Opus 5.5 <[email protected]>
127 lines
4.9 KiB
Go
127 lines
4.9 KiB
Go
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)
|
|
}
|
|
}
|