CI / build (push) Successful in 34s
The invitation carries the room as the dialog shows it (name, owner, image, occupancy, description) and the inviter's picture, age and gender from IMVU's public profile. Accepting names the room (room_instance_id), so the client joins it as a public room with the invite rather than opening a private chat, and getParticipants before joining asks the hub, so the client sees its inviter there. Co-Authored-By: Claude Opus 5.5 <[email protected]>
444 lines
20 KiB
Go
444 lines
20 KiB
Go
package hosted
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.t5de.dev/t5de/t5de-proxy/internal/imq"
|
|
"git.t5de.dev/t5de/t5de-proxy/internal/proxy"
|
|
"git.t5de.dev/t5de/t5de-proxy/internal/xmlrpc"
|
|
)
|
|
|
|
// fakeHub answers joins at once (through rooms.FromHub) and records what it's sent.
|
|
type fakeHub struct {
|
|
mu sync.Mutex
|
|
rooms *Rooms
|
|
sent []map[string]any
|
|
scene []any
|
|
}
|
|
|
|
func (h *fakeHub) Send(v any) bool {
|
|
m := v.(map[string]any)
|
|
h.mu.Lock()
|
|
h.sent = append(h.sent, m)
|
|
h.mu.Unlock()
|
|
if m["type"] == "room.join" {
|
|
n := m["room"].(int64)
|
|
var answer string
|
|
if n == 9 {
|
|
answer = `{"type": "room.error", "room": 9, "reason": "full", "message": "This room is full."}`
|
|
} else {
|
|
answer = fmt.Sprintf(`{"type": "room.joined", "room": %d, "chat": %d, "sequence": 4,
|
|
"participants": [{"imvu_id": 7, "seat": 1}, {"imvu_id": 88, "seat": 2}]}`, n, Base+n)
|
|
}
|
|
go h.rooms.FromHub([]byte(answer))
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (h *fakeHub) Call(ctx context.Context, method, path string, body, out any) error {
|
|
switch {
|
|
case method == "GET" && path == "/api/v1/hosted-rooms/3":
|
|
return json.Unmarshal([]byte(`{"room": {"n": 3, "name": "Lounge <b>", "ownerImvuId": 88, "ownerName": "Erin",
|
|
"roomPid": 50168116, "rating": "general", "maxUsers": 10, "occupancy": 2},
|
|
"scene": {"contents": [[1, 47477426, "37207-46"], [2, "coatcoat22", "37-11"], [3, "notherenn2", "37-12"]], "hash": "h1"}}`), out)
|
|
case method == "GET" && strings.HasPrefix(path, "/api/v1/decorations?instances="):
|
|
return json.Unmarshal([]byte(`{"rooms": [{"instance": "1-644", "contents": [[1, "coatcoat22", "x"]]}, {"instance": "2-17", "contents": []}]}`), out)
|
|
case method == "GET" && path == "/api/v1/hosted-rooms/3/participants":
|
|
return json.Unmarshal([]byte(`{"participants": [{"imvu_id": 88, "seat": 1, "name": "Erin<"}]}`), out)
|
|
case method == "GET" && path == "/api/v1/hosted-rooms":
|
|
return json.Unmarshal([]byte(`{"rooms": [{"n": 3, "name": "Lounge", "ownerImvuId": 88, "ownerName": "Erin", "maxUsers": 10, "occupancy": 2}]}`), out)
|
|
case (method == "PUT" || method == "DELETE") && (path == "/api/v1/hosted-rooms/3/favorite" || path == "/api/v1/hosted-rooms/3/rating"):
|
|
h.mu.Lock()
|
|
h.sent = append(h.sent, map[string]any{"type": method + " " + path, "body": body})
|
|
h.mu.Unlock()
|
|
return nil
|
|
case method == "PUT" && path == "/api/v1/hosted-rooms/3/scene":
|
|
h.mu.Lock()
|
|
h.scene = append(h.scene, body)
|
|
h.mu.Unlock()
|
|
return nil
|
|
}
|
|
return fmt.Errorf("unexpected %s %s", method, path)
|
|
}
|
|
|
|
func (h *fakeHub) sentOf(kind string) []map[string]any {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
var out []map[string]any
|
|
for _, m := range h.sent {
|
|
if m["type"] == kind {
|
|
out = append(out, m)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func setup() (*Rooms, *fakeHub, *[]string) {
|
|
h := &fakeHub{}
|
|
var events []string
|
|
r := &Rooms{Hub: h, User: func() int64 { return 7 },
|
|
OnJoin: func(chat, room string) { events = append(events, "join "+chat+" "+room) },
|
|
OnLeave: func(chat string) { events = append(events, "leave "+chat) },
|
|
LocalProduct: func(id string) (int64, bool) { return 2000000005, id == "coatcoat22" },
|
|
GlobalProduct: func(local int64) (string, bool) { return "coatcoat22", local == 2000000005 }}
|
|
h.rooms = r
|
|
return r, h, &events
|
|
}
|
|
|
|
func xmlrpcCall(t *testing.T, r *Rooms, host, method string, args xmlrpc.Struct) (*xmlrpc.Response, bool) {
|
|
t.Helper()
|
|
body := (&xmlrpc.Call{Method: method, Params: []xmlrpc.Value{args}}).Marshal()
|
|
u, _ := url.Parse("https://" + host + "/api/xmlrpc/" + map[bool]string{true: "chat.php", false: "client.php"}[strings.HasPrefix(method, "chat.")])
|
|
req, _ := http.NewRequest("POST", u.String(), bytes.NewReader(body))
|
|
x := &proxy.Exchange{URL: u, Request: req, Body: body}
|
|
r.Intercept(x)
|
|
if x.Response == nil {
|
|
return nil, false
|
|
}
|
|
b, _ := io.ReadAll(x.Response.Body)
|
|
resp, err := xmlrpc.ParseResponse(b)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v\n%s", method, err, b)
|
|
}
|
|
return resp, true
|
|
}
|
|
|
|
func get(v xmlrpc.Value, name string) xmlrpc.Value {
|
|
s, _ := v.(xmlrpc.Struct)
|
|
out, _ := s.Get(name)
|
|
return out
|
|
}
|
|
|
|
// frames reads the frames the proxy sent the client.
|
|
func frames(t *testing.T, b []byte) []imq.Frame {
|
|
var out []imq.Frame
|
|
for len(b) > 0 {
|
|
f, n, err := imq.Next(b)
|
|
if err != nil || n == 0 {
|
|
t.Fatalf("frames: %v", err)
|
|
}
|
|
out = append(out, f)
|
|
b = b[n:]
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestHostedRoom(t *testing.T) {
|
|
r, h, events := setup()
|
|
|
|
// IMVU's rooms are left alone.
|
|
if _, answered := xmlrpcCall(t, r, "chat.imvu.com", "chat.getOrMakeChat", xmlrpc.Struct{{Name: "activity", Value: "publicroom-170879926-5533"}}); answered {
|
|
t.Fatal("answered an IMVU room")
|
|
}
|
|
|
|
// Joining: the hub lets the client in; it sits in the first free seat.
|
|
resp, _ := xmlrpcCall(t, r, "chat.imvu.com", "chat.getOrMakeChat", xmlrpc.Struct{{Name: "activity", Value: "publicroom-88-2100000003"}})
|
|
if get(resp.Params[0], "chatId") != int64(2100000003) || get(resp.Params[0], "seat") != int64(1) || get(resp.Params[0], "lastMessageId") != int64(4) {
|
|
t.Fatalf("join answer %v", resp.Params[0])
|
|
}
|
|
if len(*events) != 1 || (*events)[0] != "join 2100000003 88-2100000003" {
|
|
t.Fatalf("events %v", *events)
|
|
}
|
|
// A full room: the client is told why.
|
|
if resp, _ := xmlrpcCall(t, r, "chat.imvu.com", "chat.getOrMakeChat", xmlrpc.Struct{{Name: "activity", Value: "publicroom-88-2100000009"}}); get(resp.Params[0], "response") != "declined" || get(resp.Params[0], "explanation") != "This room is full." {
|
|
t.Fatalf("full room: %+v", resp)
|
|
}
|
|
|
|
// Others in the room, without the client.
|
|
resp, _ = xmlrpcCall(t, r, "chat.imvu.com", "chat.getParticipants", xmlrpc.Struct{{Name: "userId", Value: int64(7)}, {Name: "chatId", Value: int64(2100000003)}})
|
|
others := resp.Params[0].([]xmlrpc.Value)
|
|
if len(others) != 1 || get(others[0], "userId") != int64(88) {
|
|
t.Fatalf("participants %v", others)
|
|
}
|
|
|
|
// IMQ: the room's queues are answered here; IMVU gets the rest.
|
|
var toClient bytes.Buffer
|
|
c := proxy.NewIMQConn(1, &toClient)
|
|
sub := imq.Subscription{Queues: []string{"/user/7"}, WithResults: []imq.QueueOp{{Queue: "/chat/2100000003", OpID: 5}, {Queue: "/chat/1385863547", OpID: 6}}}
|
|
rest := r.Route(c, false, sub)
|
|
if fmt.Sprint(rest.All()) != "[/user/7 /chat/1385863547]" {
|
|
t.Fatalf("for IMVU: %v", rest.All())
|
|
}
|
|
var types []string
|
|
for _, f := range frames(t, toClient.Bytes()) {
|
|
types = append(types, imq.TypeName(f.Type))
|
|
}
|
|
if got := strings.Join(types, " "); got != "G2cJoinedQueue G2cCreateMount G2cCreateMount G2cCreateMount G2cResult" {
|
|
t.Fatalf("frames %s", got)
|
|
}
|
|
|
|
// Chat goes to the hub; sitting down too.
|
|
if !r.Send("/chat/2100000003", "messages", []byte(`{"message": "*seat 2", "userId": 7}`), []byte(`{"message": "*seat 2", "userId": 7}`), false) || r.Send("/chat/1385863547", "messages", []byte(`{}`), []byte(`{}`), false) {
|
|
t.Fatal("Send's answer")
|
|
}
|
|
r.Send("/chat/2100000003", "messages", []byte(`{"message": "*msg SeatAssignment 0 7 3 0", "userId": 7}`), nil, false)
|
|
r.Send("/chat/2100000003", "messages", []byte(`{"message": "*msg SeatAssignment 0 7 1 5521", "userId": 7}`), nil, false) // on furniture
|
|
// The seat chosen on joining (1), "*seat 2", and the room seat 3.
|
|
if seats, sends := h.sentOf("room.seat"), h.sentOf("room.send"); len(seats) != 3 || seats[0]["seat"] != 1 || seats[1]["seat"] != 2 || seats[2]["seat"] != 3 || len(sends) != 3 {
|
|
t.Fatalf("seats %v, sends %v", seats, sends)
|
|
}
|
|
|
|
// Others' chat comes back as the gateway would deliver it.
|
|
toClient.Reset()
|
|
r.FromHub([]byte(`{"type": "room.message", "room": 3, "from": 88, "mount": "messages", "message": "{\"message\":\"hi\",\"userId\":88}", "sequence": 5}`))
|
|
fs := frames(t, toClient.Bytes())
|
|
if len(fs) != 1 {
|
|
t.Fatalf("%d frames", len(fs))
|
|
}
|
|
d, _ := imq.ParseDelivered(fs[0].Data)
|
|
if d.UserID != "/user/88" || d.Queue != "/chat/2100000003" || d.Sequence != 5 || !strings.Contains(string(d.Message), "hi") {
|
|
t.Fatalf("delivered %+v", d)
|
|
}
|
|
|
|
// The room's card and scene come from the hub; the scene's community products as local IDs (missing ones left out).
|
|
resp, _ = xmlrpcCall(t, r, "client-dynamic.imvu.com", "test.getRoomCardInfo", xmlrpc.Struct{{Name: "room_instance_id", Value: "88-2100000003"}})
|
|
if get(resp.Params[0], "name") != "Lounge b" || get(resp.Params[0], "owner_cid") != int64(88) {
|
|
t.Fatalf("card %v", resp.Params[0])
|
|
}
|
|
resp, _ = xmlrpcCall(t, r, "client-dynamic.imvu.com", "test.getRoomState", xmlrpc.Struct{{Name: "room_instance_id", Value: "88-2100000003"}})
|
|
contents := get(get(resp.Params[0], "room_state"), "room_contents").([]xmlrpc.Value)
|
|
if len(contents) != 2 || fmt.Sprint(contents[1]) != "[2 2000000005 37-11]" || get(get(get(resp.Params[0], "room_state"), "room_info"), "room_pid") != "50168116" {
|
|
t.Fatalf("scene %v", resp.Params[0])
|
|
}
|
|
// Saving the scene: community products as the hub's IDs.
|
|
xmlrpcCall(t, r, "client-dynamic.imvu.com", "test.setRoomState", xmlrpc.Struct{{Name: "room_instance_id", Value: "88-2100000003"},
|
|
{Name: "room_contents", Value: []xmlrpc.Value{[]xmlrpc.Value{int64(1), int64(47477426), "37-1"}, []xmlrpc.Value{int64(2), int64(2000000005), "37-2"}}}})
|
|
if b, _ := json.Marshal(h.scene); string(b) != `[{"contents":[[1,47477426,"37-1"],[2,"coatcoat22","37-2"]]}]` {
|
|
t.Fatalf("saved %s", b)
|
|
}
|
|
|
|
// The participant post for a hosted chat is answered here.
|
|
u, _ := url.Parse("https://api.imvu.com/chat/chat-2100000003/participants/user-7")
|
|
req, _ := http.NewRequest("POST", u.String(), nil)
|
|
x := &proxy.Exchange{URL: u, Request: req}
|
|
r.Intercept(x)
|
|
if x.Response == nil || x.Response.StatusCode != 200 {
|
|
t.Fatal("participant post not answered")
|
|
}
|
|
|
|
// Anything else naming a hosted room is answered here, not sent to IMVU.
|
|
for _, raw := range []string{"https://api.imvu.com/room/room-88-2100000003", "https://client-dynamic.imvu.com/api/rooms/room_rating.php?room_instance_id=88-2100000003",
|
|
"https://client-dynamic.imvu.com/api/whatever.php?chat=2100000003"} {
|
|
u, _ := url.Parse(raw)
|
|
req, _ := http.NewRequest("GET", u.String(), nil)
|
|
x := &proxy.Exchange{URL: u, Request: req}
|
|
r.Intercept(x)
|
|
if x.Response == nil {
|
|
t.Errorf("%s went to IMVU", raw)
|
|
}
|
|
}
|
|
|
|
// Leaving.
|
|
xmlrpcCall(t, r, "chat.imvu.com", "chat.terminateChat", xmlrpc.Struct{{Name: "userId", Value: int64(7)}, {Name: "chatId", Value: int64(2100000003)}})
|
|
if len(h.sentOf("room.leave")) != 1 || (*events)[len(*events)-1] != "leave 2100000003" {
|
|
t.Fatalf("leave: %v %v", h.sentOf("room.leave"), *events)
|
|
}
|
|
}
|
|
|
|
func TestRoomList(t *testing.T) {
|
|
r, _, _ := setup()
|
|
u, _ := url.Parse("https://client-dynamic.imvu.com/api/rooms/rooms_list_paginated.php?page=1")
|
|
req, _ := http.NewRequest("GET", u.String(), nil)
|
|
x := &proxy.Exchange{URL: u, Request: req}
|
|
r.Intercept(x)
|
|
body := []byte(`{"customers_room_id": ["644", "17"], "customers_id": ["1", "2"], "num_participants": [4, 1], "name": ["A", "B"], "participants": [[], []], "is_ap": ["0", "0"], "odd": [{"x": 1}, {}], "number_of_rooms": 2}`)
|
|
out, err := x.ModifyResponse(&http.Response{StatusCode: 200}, body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var l map[string]any
|
|
json.Unmarshal(out, &l)
|
|
if fmt.Sprint(l["customers_room_id"]) != "[644 17 2100000003]" || fmt.Sprint(l["customers_id"]) != "[1 2 88]" ||
|
|
fmt.Sprint(l["num_participants"]) != "[4 1 2]" || fmt.Sprint(l["odd"]) != "[map[x:1] map[] map[]]" || l["number_of_rooms"] != float64(2) ||
|
|
fmt.Sprint(l["t5de_t5"]) != "[1 0 1]" { // decorated, plain, hosted
|
|
t.Fatalf("list %s", out)
|
|
}
|
|
// Other pages get no hosted rooms, but their decorated rooms are marked.
|
|
u, _ = url.Parse("https://client-dynamic.imvu.com/api/rooms/rooms_list_paginated.php?page=2")
|
|
x = &proxy.Exchange{URL: u, Request: req}
|
|
r.Intercept(x)
|
|
out, _ = x.ModifyResponse(&http.Response{StatusCode: 200}, body)
|
|
json.Unmarshal(out, &l)
|
|
if fmt.Sprint(l["customers_room_id"]) != "[644 17]" || fmt.Sprint(l["t5de_t5"]) != "[1 0]" {
|
|
t.Fatalf("page 2: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestBadgesAndRoomInfo(t *testing.T) {
|
|
r, _, _ := setup()
|
|
do := func(raw string, body []byte, modified []byte) []byte {
|
|
t.Helper()
|
|
u, _ := url.Parse(raw)
|
|
req, _ := http.NewRequest("POST", u.String(), bytes.NewReader(body))
|
|
x := &proxy.Exchange{URL: u, Request: req, Body: body}
|
|
r.Intercept(x)
|
|
if x.Response != nil {
|
|
b, _ := io.ReadAll(x.Response.Body)
|
|
return b
|
|
}
|
|
if x.ModifyResponse == nil {
|
|
t.Fatalf("%s: left alone", raw)
|
|
}
|
|
out, _ := x.ModifyResponse(&http.Response{StatusCode: 200}, modified)
|
|
return out
|
|
}
|
|
// A decorated IMVU room's card gets t5de_t5.
|
|
card := (&xmlrpc.Call{Method: "test.getRoomCardInfo", Params: []xmlrpc.Value{xmlrpc.Struct{{Name: "room_instance_id", Value: "1-644"}}}}).Marshal()
|
|
answer := (&xmlrpc.Response{Params: []xmlrpc.Value{xmlrpc.Struct{{Name: "name", Value: "A"}}}}).Marshal()
|
|
if out := do("https://client-dynamic.imvu.com/api/xmlrpc/client.php", card, answer); !strings.Contains(string(out), "t5de_t5") {
|
|
t.Fatalf("card %s", out)
|
|
}
|
|
// The Manage tab's own decorated rooms get it too.
|
|
managed := []byte(`{"result": {"limit": 1, "rooms": [{"room_instance_id": "1-644"}, {"room_instance_id": "2-17"}]}}`)
|
|
out := do("https://client-dynamic.imvu.com/api/room_management_info.php", nil, managed)
|
|
if !strings.Contains(string(out), `{"room_instance_id":"1-644","t5de_t5":1}`) || strings.Count(string(out), "t5de_t5") != 1 {
|
|
t.Fatalf("managed %s", out)
|
|
}
|
|
// A decorated IMVU room's room_info.php gets t5de_t5.
|
|
if out := do("https://client-dynamic.imvu.com/api/rooms/room_info.php?room_id=1-644", nil, []byte(`{"participants": []}`)); !strings.Contains(string(out), `"t5de_t5":1`) {
|
|
t.Fatalf("room info %s", out)
|
|
}
|
|
// room_info.php for a hosted room: its people, from the hub.
|
|
out = do("https://client-dynamic.imvu.com/api/rooms/room_info.php?room_id=88-2100000003", nil, nil)
|
|
if !strings.Contains(string(out), `"avatar_name":"Erin"`) || !strings.Contains(string(out), `"customers_id":"88"`) ||
|
|
!strings.Contains(string(out), `"max_users":10`) || !strings.Contains(string(out), `"t5de_t5":1`) {
|
|
t.Fatalf("room info %s", out)
|
|
}
|
|
}
|
|
|
|
func TestPhase2(t *testing.T) {
|
|
r, h, _ := setup()
|
|
imvu := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
fmt.Fprintf(w, `{"denormalized": {"https://api.imvu.com/user/user-88": {"data": {"username": "Erin", "age": 30, "gender": "f", "thumbnail_url": "https://x/p.png"}}}}`)
|
|
}))
|
|
defer imvu.Close()
|
|
r.HTTPClient = &http.Client{Transport: rewrite{imvu.URL}}
|
|
var userMsgs []string
|
|
r.UserDeliver = func(mount string, b []byte) error { userMsgs = append(userMsgs, mount+" "+string(b)); return nil }
|
|
call := func(method string, args xmlrpc.Struct) xmlrpc.Value {
|
|
t.Helper()
|
|
resp, ok := xmlrpcCall(t, r, "chat.imvu.com", method, args)
|
|
if !ok {
|
|
t.Fatalf("%s left to IMVU", method)
|
|
}
|
|
return resp.Params[0]
|
|
}
|
|
xmlrpcCall(t, r, "chat.imvu.com", "chat.getOrMakeChat", xmlrpc.Struct{{Name: "activity", Value: "publicroom-88-2100000003"}})
|
|
|
|
// Inviting: waiting until the hub says it was accepted.
|
|
first := call("chat.attemptInvite", xmlrpc.Struct{{Name: "partnerId", Value: int64(55)}, {Name: "location", Value: "88-2100000003"}})
|
|
id := get(first, "inviteId")
|
|
if get(first, "waiting") == nil || len(h.sentOf("room.invite")) != 1 || h.sentOf("room.invite")[0]["imvu_id"] != int64(55) {
|
|
t.Fatalf("first attempt %v, sent %v", first, h.sentOf("room.invite"))
|
|
}
|
|
r.FromHub([]byte(`{"type": "room.invite_result", "room": 3, "imvu_id": 55, "ok": true, "invite": "abc"}`))
|
|
if again := call("chat.attemptInvite", xmlrpc.Struct{{Name: "inviteId", Value: id}, {Name: "location", Value: "88-2100000003"}}); get(again, "waiting") == nil {
|
|
t.Fatalf("still waiting expected: %v", again)
|
|
}
|
|
r.FromHub([]byte(`{"type": "room.invite_answer", "invite": "abc", "to": 55, "accept": true}`))
|
|
if done := call("chat.attemptInvite", xmlrpc.Struct{{Name: "inviteId", Value: id}, {Name: "location", Value: "88-2100000003"}}); get(done, "waiting") != nil || get(done, "response") != nil {
|
|
t.Fatalf("accepted expected: %v", done)
|
|
}
|
|
|
|
// Invited: the client gets chatInvite on its user queue; accepting joins with the invite.
|
|
r.FromHub([]byte(`{"type": "room.invited", "room": 3, "invite": "xyz", "from": 88, "room_name": "Lounge"}`))
|
|
for i := 0; i < 100 && len(userMsgs) == 0; i++ {
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
if len(userMsgs) != 1 || !strings.HasPrefix(userMsgs[0], "web_msg chatInvite ") || !strings.Contains(userMsgs[0], `"room_instance_id":"88-2100000003"`) || !strings.Contains(userMsgs[0], `"maxOccupancy":10`) || !strings.Contains(userMsgs[0], `"gender":"f"`) || !strings.Contains(userMsgs[0], `"name":"Erin"`) {
|
|
t.Fatalf("invitation %v", userMsgs)
|
|
}
|
|
var inv map[string]any
|
|
json.Unmarshal([]byte(strings.TrimPrefix(userMsgs[0], "web_msg chatInvite ")), &inv)
|
|
// Before accepting (not in the room), getParticipants says who's in it, so the client sees its inviter there.
|
|
r.leave(3)
|
|
if ps := call("chat.getParticipants", xmlrpc.Struct{{Name: "chatId", Value: int64(2100000003)}}); !strings.Contains(fmt.Sprint(ps), "88") {
|
|
t.Fatalf("participants before joining %v", ps)
|
|
}
|
|
accepted := call("chat.acceptInvite", xmlrpc.Struct{{Name: "inviteId", Value: int64(inv["inviteId"].(float64))}, {Name: "chatId", Value: int64(2100000003)}})
|
|
if get(accepted, "room_instance_id") != "88-2100000003" || h.sentOf("room.invite_reply")[0]["accept"] != true {
|
|
t.Fatalf("accepting: %v, sent %v", accepted, h.sent)
|
|
}
|
|
// The client then joins the room, with the invite.
|
|
xmlrpcCall(t, r, "chat.imvu.com", "chat.getOrMakeChat", xmlrpc.Struct{{Name: "activity", Value: "publicroom-88-2100000003"}})
|
|
if joins := h.sentOf("room.join"); joins[len(joins)-1]["invite"] != "xyz" {
|
|
t.Fatalf("join after accepting %v", joins)
|
|
}
|
|
|
|
// Booting: the *boot line also tells the hub; bootOutOfChat is answered here.
|
|
r.Send("/chat/2100000003", "messages", []byte(`{"message": "*boot 7 88", "userId": 7}`), nil, false)
|
|
if boots := h.sentOf("room.boot"); len(boots) != 1 || boots[0]["imvu_id"] != int64(88) {
|
|
t.Fatalf("boot %v", boots)
|
|
}
|
|
call("chat.bootOutOfChat", xmlrpc.Struct{{Name: "userId", Value: int64(88)}, {Name: "roomInstanceId", Value: "88-2100000003"}})
|
|
|
|
// Favorites.
|
|
resp, _ := xmlrpcCall(t, r, "client-dynamic.imvu.com", "test.addFavoriteRoom", xmlrpc.Struct{{Name: "room_instance_id", Value: "88-2100000003"}})
|
|
if resp == nil || len(h.sentOf("PUT /api/v1/hosted-rooms/3/favorite")) != 1 {
|
|
t.Fatal("favorite not added")
|
|
}
|
|
|
|
// Favorites from the room list.
|
|
fu, _ := url.Parse("https://client-dynamic.imvu.com/api/favorite_rooms.php")
|
|
fbody := []byte(`{"action": "remove", "room_instance_id": "88-2100000003"}`)
|
|
freq, _ := http.NewRequest("POST", fu.String(), bytes.NewReader(fbody))
|
|
fx := &proxy.Exchange{URL: fu, Request: freq, Body: fbody}
|
|
r.Intercept(fx)
|
|
if fx.Response == nil || len(h.sentOf("DELETE /api/v1/hosted-rooms/3/favorite")) != 1 {
|
|
t.Fatal("favorite not removed")
|
|
}
|
|
|
|
// Rating.
|
|
u, _ := url.Parse("https://client-dynamic.imvu.com/api/rooms/room_rating.php")
|
|
body := []byte("room_instance_id=88-2100000003&score=4")
|
|
req, _ := http.NewRequest("POST", u.String(), bytes.NewReader(body))
|
|
x := &proxy.Exchange{URL: u, Request: req, Body: body}
|
|
r.Intercept(x)
|
|
if x.Response == nil || len(h.sentOf("PUT /api/v1/hosted-rooms/3/rating")) != 1 {
|
|
t.Fatal("rating not sent")
|
|
}
|
|
|
|
// Occupancy: watching tells the hub; changes reach the watcher as state changes.
|
|
var toClient bytes.Buffer
|
|
c := proxy.NewIMQConn(2, &toClient)
|
|
r.Route(c, false, imq.Subscription{Queues: []string{"room_occupancy/88-2100000003"}})
|
|
if w := h.sentOf("room.watch"); len(w) == 0 || fmt.Sprint(w[len(w)-1]["rooms"]) != "[3]" {
|
|
t.Fatalf("watch %v", w)
|
|
}
|
|
toClient.Reset()
|
|
r.FromHub([]byte(`{"type": "room.occupancy", "room": 3, "participants": [{"imvu_id": 88, "seat": 1}, {"imvu_id": 7, "seat": 2}]}`))
|
|
fs := frames(t, toClient.Bytes())
|
|
if len(fs) != 1 || fs[0].Type != imq.G2cStateChange || !strings.Contains(string(fs[0].Data), "room_occupancy/88-2100000003") {
|
|
t.Fatalf("occupancy frames %v", fs)
|
|
}
|
|
}
|
|
|
|
func TestAppendRoomToEmptyList(t *testing.T) {
|
|
list := map[string]json.RawMessage{"customers_id": json.RawMessage(`[]`), "is_ap": json.RawMessage(`[]`), "is_vip": json.RawMessage(`[]`), "name": json.RawMessage(`[]`)}
|
|
appendRoom(list, Room{N: 3, OwnerImvuID: 88, Name: "Lounge"})
|
|
if string(list["is_ap"]) != "[0]" || string(list["is_vip"]) != "[0]" || string(list["name"]) != `["Lounge"]` {
|
|
t.Fatalf("list %s %s %s", list["is_ap"], list["is_vip"], list["name"])
|
|
}
|
|
}
|
|
|
|
// rewrite sends every request to a test server.
|
|
type rewrite struct{ to string }
|
|
|
|
func (t rewrite) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
u, _ := url.Parse(t.to + req.URL.Path)
|
|
req = req.Clone(req.Context())
|
|
req.URL, req.Host = u, u.Host
|
|
return http.DefaultTransport.RoundTrip(req)
|
|
}
|