package adminapi_test import ( "net/http" "net/url" "testing" "github.com/dariuszjaniak/warp/server/internal/adminapi" "github.com/dariuszjaniak/warp/server/internal/httputil" "github.com/dariuszjaniak/warp/server/internal/roomserver" "github.com/dariuszjaniak/warp/server/internal/settings" ) // createRoom makes a room through the roomserver directly: the admin API // has no createRoom, and would be the wrong place for one. func (ts *testServer) createRoom(creator string, req roomserver.CreateRoomRequest) string { ts.t.Helper() req.Creator = "@" + creator + ":" + testServerName resp, err := ts.rooms.CreateRoom(ts.t.Context(), req) if err != nil { ts.t.Fatalf("CreateRoom: %v", err) } return resp.RoomID } func TestAdminRoomList(t *testing.T) { ts := newTestServer(t, settings.Defaults{}) token := ts.admin("admin") ts.user("alice") roomID := ts.createRoom("alice", roomserver.CreateRoomRequest{ Name: "Operations", Preset: roomserver.PresetPublicChat, Visibility: roomserver.VisibilityPublic, }) ts.createRoom("alice", roomserver.CreateRoomRequest{Name: "Quiet"}) rec := ts.do(http.MethodGet, adminapi.Prefix+"/rooms", token, nil) expectStatus(t, rec, http.StatusOK) body := jsonBody(t, rec) if total, _ := body["total"].(float64); total != 2 { t.Errorf("total = %v, want 2", body["total"]) } rooms, _ := body["rooms"].([]any) if len(rooms) != 2 { t.Fatalf("the list has %d rooms, want 2: %s", len(rooms), rec.Body.String()) } // The search covers the name, which is the column the panel sorts by. rec = ts.do(http.MethodGet, adminapi.Prefix+"/rooms?search=operations", token, nil) expectStatus(t, rec, http.StatusOK) body = jsonBody(t, rec) rooms, _ = body["rooms"].([]any) if len(rooms) != 1 { t.Fatalf("the search returned %d rooms, want 1", len(rooms)) } found, _ := rooms[0].(map[string]any) if found["room_id"] != roomID { t.Errorf("the search found %v, want %q", found["room_id"], roomID) } if found["published"] != true { t.Errorf("the published flag is %v, want true", found["published"]) } if found["joined_members"] != float64(1) { t.Errorf("joined_members = %v, want 1", found["joined_members"]) } // An ordinary user is refused, and told why rather than given a 404. rec = ts.do(http.MethodGet, adminapi.Prefix+"/rooms", ts.user("bob"), nil) expectError(t, rec, http.StatusForbidden, httputil.ErrCodeForbidden) rec = ts.do(http.MethodGet, adminapi.Prefix+"/rooms?limit=lots", token, nil) expectError(t, rec, http.StatusBadRequest, httputil.ErrCodeInvalidParam) } func TestAdminRoomDetail(t *testing.T) { ts := newTestServer(t, settings.Defaults{}) token := ts.admin("admin") ts.user("alice") roomID := ts.createRoom("alice", roomserver.CreateRoomRequest{ Name: "Detailed", Preset: roomserver.PresetPublicChat, RoomAliasName: "detailed", }) rec := ts.do(http.MethodGet, adminapi.Prefix+"/rooms/"+url.PathEscape(roomID), token, nil) expectStatus(t, rec, http.StatusOK) body := jsonBody(t, rec) if body["room_id"] != roomID || body["name"] != "Detailed" { t.Errorf("the detail is %s", rec.Body.String()) } if body["creator"] != "@alice:"+testServerName { t.Errorf("creator = %v", body["creator"]) } if body["members"] != float64(1) || body["joined_members"] != float64(1) { t.Errorf("members = %v, joined = %v", body["members"], body["joined_members"]) } aliases, _ := body["aliases"].([]any) if len(aliases) != 1 { t.Errorf("aliases = %v, want one", body["aliases"]) } // A room filled by one server ends at a single event. extremities, _ := body["forward_extremities"].([]any) if len(extremities) != 1 { t.Errorf("forward_extremities = %v, want one", body["forward_extremities"]) } rec = ts.do(http.MethodGet, adminapi.Prefix+"/rooms/"+ url.PathEscape("!nope:"+testServerName), token, nil) expectError(t, rec, http.StatusNotFound, httputil.ErrCodeNotFound) rec = ts.do(http.MethodGet, adminapi.Prefix+"/rooms/not-a-room", token, nil) expectError(t, rec, http.StatusBadRequest, httputil.ErrCodeInvalidParam) } func TestAdminPurgeRoom(t *testing.T) { ts := newTestServer(t, settings.Defaults{}) token := ts.admin("admin") ts.user("alice") roomID := ts.createRoom("alice", roomserver.CreateRoomRequest{Name: "Doomed"}) escaped := url.PathEscape(roomID) // An ordinary user cannot destroy a room. rec := ts.do(http.MethodDelete, adminapi.Prefix+"/rooms/"+escaped, ts.user("bob"), nil) expectError(t, rec, http.StatusForbidden, httputil.ErrCodeForbidden) rec = ts.do(http.MethodDelete, adminapi.Prefix+"/rooms/"+escaped, token, nil) expectStatus(t, rec, http.StatusOK) rec = ts.do(http.MethodGet, adminapi.Prefix+"/rooms/"+escaped, token, nil) expectError(t, rec, http.StatusNotFound, httputil.ErrCodeNotFound) // Purging twice is a 404, not a silent success: an operator who sees // an OK has to be able to believe the room was there. rec = ts.do(http.MethodDelete, adminapi.Prefix+"/rooms/"+escaped, token, nil) expectError(t, rec, http.StatusNotFound, httputil.ErrCodeNotFound) } func TestAdminForceLeave(t *testing.T) { ts := newTestServer(t, settings.Defaults{}) token := ts.admin("admin") ts.user("alice") ts.user("bob") roomID := ts.createRoom("alice", roomserver.CreateRoomRequest{Preset: roomserver.PresetPublicChat}) escaped := url.PathEscape(roomID) bobID := "@bob:" + testServerName if _, err := ts.rooms.JoinRoom(t.Context(), roomserver.JoinRequest{ RoomID: roomID, User: bobID, }); err != nil { t.Fatalf("bob joining: %v", err) } rec := ts.do(http.MethodPost, adminapi.Prefix+"/rooms/"+escaped+"/force_leave", token, map[string]any{"user_id": bobID}) expectStatus(t, rec, http.StatusOK) membership, err := ts.rooms.Membership(t.Context(), roomID, bobID) if err != nil { t.Fatalf("Membership: %v", err) } if membership != "leave" { t.Errorf("bob's membership is %q, want leave", membership) } // A user who is not in the room has nothing to leave. rec = ts.do(http.MethodPost, adminapi.Prefix+"/rooms/"+escaped+"/force_leave", token, map[string]any{"user_id": "@carol:" + testServerName}) expectError(t, rec, http.StatusForbidden, httputil.ErrCodeForbidden) rec = ts.do(http.MethodPost, adminapi.Prefix+"/rooms/"+escaped+"/force_leave", token, map[string]any{}) expectError(t, rec, http.StatusBadRequest, httputil.ErrCodeMissingParam) } // TestAdminStatsCountsRooms checks the dashboard counters follow the rooms. func TestAdminStatsCountsRooms(t *testing.T) { ts := newTestServer(t, settings.Defaults{}) token := ts.admin("admin") ts.user("alice") rec := ts.do(http.MethodGet, adminapi.Prefix+"/stats", token, nil) expectStatus(t, rec, http.StatusOK) if rooms, _ := jsonBody(t, rec)["rooms"].(float64); rooms != 0 { t.Errorf("a fresh server reports %v rooms, want 0", rooms) } ts.createRoom("alice", roomserver.CreateRoomRequest{ Preset: roomserver.PresetPublicChat, Visibility: roomserver.VisibilityPublic, }) rec = ts.do(http.MethodGet, adminapi.Prefix+"/stats", token, nil) expectStatus(t, rec, http.StatusOK) body := jsonBody(t, rec) if rooms, _ := body["rooms"].(float64); rooms != 1 { t.Errorf("rooms = %v, want 1", body["rooms"]) } if published, _ := body["published_rooms"].(float64); published != 1 { t.Errorf("published_rooms = %v, want 1", body["published_rooms"]) } if events, _ := body["events"].(float64); events == 0 { t.Error("a server with a room reports no events") } }