CI / build (push) Successful in 36s
The client's own room actions work in hosted rooms, through the hub (t5de-hub's docs/hosted-rooms.md,
Phase 2): a "*boot" line goes to the room, then to the hub as room.boot; refused joins are declined as
the client's own are (booted ones as booted). Invitations: chat.attemptInvite polls the hub's answer,
and the invitee's client gets chatInvite on its user queue, accepting with the invite. Favorites
(favorite_rooms.php, add/removeFavoriteRoom, the favorites list), ratings (room_rating.php), the room
card's moderators, image and score, and room_occupancy queues fed from room.watch.
Web API parameters are read from JSON bodies too (imvu.http.securePost), and hosted rows added to an
empty IMVU list keep their flags numeric ("0" counted as set).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
418 lines
19 KiB
Go
418 lines
19 KiB
Go
package hosted
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"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()
|
|
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], `"location":"88-2100000003"`) || !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)
|
|
accepted := call("chat.acceptInvite", xmlrpc.Struct{{Name: "inviteId", Value: int64(inv["inviteId"].(float64))}, {Name: "chatId", Value: int64(2100000003)}})
|
|
if get(accepted, "chatId") != int64(2100000003) || h.sentOf("room.invite_reply")[0]["accept"] != true || h.sentOf("room.join")[len(h.sentOf("room.join"))-1]["invite"] != "xyz" {
|
|
t.Fatalf("accepting: %v, sent %v", accepted, h.sent)
|
|
}
|
|
|
|
// 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"])
|
|
}
|
|
}
|