Files
t5de-proxy/internal/hosted/manage_test.go
T
dhkatzandClaude Opus 5.5 8daf91ede3
CI / build (push) Successful in 28s
Manage T5DE rooms from the client; the AP+ badge on avatar cards
Chat Rooms > Manage (the t5de-room-manage patch) lists the person's T5DE rooms
(room_management_info.php?t5de=1) and saves them from IMVU's room dialog (room_management.php: create
with t5de=1, edit and delete naming a hosted room) as hub calls: the room, its moderators by name, and a
picture from the person's IMVU gallery, uploaded to the hub. The hub's refusals show in the dialog
(hub.Error keeps their message).

Avatar cards get show_ap_plus again, which IMVU stopped sending, from the person's public IMVU profile
(is_ap_plus).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 08:19:33 -07:00

160 lines
6.3 KiB
Go

package hosted
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"git.t5de.dev/t5de/t5de-proxy/internal/hub"
"git.t5de.dev/t5de/t5de-proxy/internal/proxy"
)
// manageHub is a hub for the Manage tab: the person's rooms, and what was asked of it.
type manageHub struct {
calls []string
bodies []map[string]any
uploads [][]byte
}
func (h *manageHub) Send(v any) bool { return true }
func (h *manageHub) Call(ctx context.Context, method, path string, body, out any) error {
h.calls = append(h.calls, method+" "+path)
b, _ := json.Marshal(body)
var m map[string]any
json.Unmarshal(b, &m)
h.bodies = append(h.bodies, m)
room := `{"room": {"n": 4, "name": "Den", "ownerImvuId": 7, "roomPid": 555, "rating": "general", "maxUsers": 6,
"privacy": "private", "moderators": [88], "image": "http://hub/api/v1/hosted-rooms/4/image?v=1"}}`
switch {
case method == "GET" && path == "/api/v1/hosted-rooms?mine=1":
return json.Unmarshal([]byte(`{"rooms": [{"n": 4, "name": "Den", "ownerImvuId": 7, "maxUsers": 6, "privacy": "private", "moderators": [88]},
{"n": 5, "name": "Other account's", "ownerImvuId": 99, "maxUsers": 10}]}`), out)
case method == "POST" && path == "/api/v1/hosted-rooms":
if m["name"] == "" {
return &hub.Error{Status: 400, Code: "invalid", Message: "A room needs a name."}
}
return json.Unmarshal([]byte(room), out)
case (method == "PATCH" || method == "DELETE") && strings.HasPrefix(path, "/api/v1/hosted-rooms/4"):
if out != nil {
return json.Unmarshal([]byte(room), out)
}
return nil
}
return fmt.Errorf("unexpected %s %s", method, path)
}
func (h *manageHub) Upload(ctx context.Context, method, path, field, filename, contentType string, file []byte, out any) error {
h.calls = append(h.calls, "UPLOAD "+path)
h.uploads = append(h.uploads, file)
return json.Unmarshal([]byte(`{"room": {"n": 4, "name": "Den", "ownerImvuId": 7, "image": "http://hub/api/v1/hosted-rooms/4/image?v=2"}}`), out)
}
func manageCall(t *testing.T, r *Rooms, path string, form url.Values) map[string]any {
t.Helper()
u, _ := url.Parse("https://client-dynamic.imvu.com" + path)
var body []byte
method := "GET"
if form != nil {
method = "POST"
m := map[string]string{}
for k := range form {
m[k] = form.Get(k)
}
body, _ = json.Marshal(m) // as imvu.http.securePost sends it
}
req, _ := http.NewRequest(method, u.String(), bytes.NewReader(body))
x := &proxy.Exchange{URL: u, Request: req, Body: body}
r.Intercept(x)
if x.Response == nil {
t.Fatalf("%s left to IMVU", path)
}
b, _ := io.ReadAll(x.Response.Body)
var out map[string]any
if err := json.Unmarshal(b, &out); err != nil {
t.Fatalf("%s: %s", path, b)
}
return out
}
func TestManage(t *testing.T) {
h := &manageHub{}
gallery := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("\x89PNG\r\n\x1a\n picture"))
}))
defer gallery.Close()
r := &Rooms{Hub: h, User: func() int64 { return 7 }, HTTPClient: &http.Client{Transport: rewrite{gallery.URL}}}
// The list: this account's rooms, as IMVU lists the person's; the cap counts the person's other rooms.
info := manageCall(t, r, "/api/room_management_info.php?t5de=1", nil)
res := info["result"].(map[string]any)
rooms := res["rooms"].([]any)
if len(rooms) != 1 || res["limit"] != float64(9) {
t.Fatalf("info %v", info)
}
row := rooms[0].(map[string]any)
if row["room_instance_id"] != "7-2100000004" || row["open"] != float64(0) || row["lieutenants"] != "88" || row["t5de_t5"] != float64(1) {
t.Fatalf("row %v", row)
}
// Creating: the room, then its moderators, then its picture from the gallery.
created := manageCall(t, r, "/api/room_management.php", url.Values{"action": {"create"}, "t5de": {"1"}, "room_name": {"Den"},
"room_pid": {"555"}, "max_users": {"6"}, "permissions": {"0"}, "ap": {"0"}, "lieutenants": {"Erin, 99"},
"image": {"http://userimages.imvu.com/userdata/p.png"}})
if created["result"].(map[string]any)["room_instance_id"] != "7-2100000004" {
t.Fatalf("created %v", created)
}
if fmt.Sprint(h.calls) != "[GET /api/v1/hosted-rooms?mine=1 POST /api/v1/hosted-rooms PATCH /api/v1/hosted-rooms/4 UPLOAD /api/v1/hosted-rooms/4/image]" {
t.Fatalf("calls %v", h.calls)
}
if b := h.bodies[1]; b["imvuId"] != float64(7) || b["privacy"] != "private" || b["roomPid"] != float64(555) || b["rating"] != "general" {
t.Fatalf("create body %v", b)
}
if fmt.Sprint(h.bodies[2]["moderators"]) != "[Erin 99]" || len(h.uploads) != 1 {
t.Fatalf("moderators %v, uploads %d", h.bodies[2], len(h.uploads))
}
// Editing with the hub's own picture keeps it; a gallery picture elsewhere than IMVU is refused.
h.calls = nil
manageCall(t, r, "/api/room_management.php", url.Values{"action": {"edit"}, "room_instance_id": {"7-2100000004"}, "room_name": {"Den 2"},
"room_pid": {"777"}, "permissions": {"2"}, "image": {"http://hub/api/v1/hosted-rooms/4/image"}})
if fmt.Sprint(h.calls) != "[PATCH /api/v1/hosted-rooms/4]" || h.bodies[len(h.bodies)-1]["privacy"] != "public" || h.bodies[len(h.bodies)-1]["roomPid"] != nil {
t.Fatalf("edit calls %v %v", h.calls, h.bodies[len(h.bodies)-1])
}
bad := manageCall(t, r, "/api/room_management.php", url.Values{"action": {"edit"}, "room_instance_id": {"7-2100000004"},
"room_name": {"Den"}, "image": {"https://evil.example/p.png"}})
if !strings.Contains(fmt.Sprint(bad["errors"]), "gallery") {
t.Fatalf("bad image %v", bad)
}
// The hub's refusals are shown in the dialog.
refused := manageCall(t, r, "/api/room_management.php", url.Values{"action": {"create"}, "t5de": {"1"}, "room_name": {""}, "room_pid": {"555"}})
if !strings.Contains(fmt.Sprint(refused["errors"]), "A room needs a name.") {
t.Fatalf("refused %v", refused)
}
// Deleting.
h.calls = nil
manageCall(t, r, "/api/room_management.php", url.Values{"action": {"delete"}, "room_instance_id": {"7-2100000004"}})
if fmt.Sprint(h.calls) != "[DELETE /api/v1/hosted-rooms/4]" {
t.Fatalf("delete %v", h.calls)
}
// IMVU's own rooms are IMVU's.
u, _ := url.Parse("https://client-dynamic.imvu.com/api/room_management.php")
body := []byte(`{"action": "edit", "room_instance_id": "7-3"}`)
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 {
t.Fatal("an IMVU room's edit was answered here")
}
}