package clientapi_test import ( "net/http" "strings" "testing" ) const profilePath = "/_matrix/client/v3/profile" func TestProfileRoundTrip(t *testing.T) { ts := newTestServer(t, nil) acc := ts.register("alice", "superuser") // A fresh account has neither field, and the spec distinguishes that // from an empty one: clients fall back to the localpart. rec := ts.get(profilePath+"/"+acc.UserID, "") expectStatus(t, rec, http.StatusOK) body := jsonBody(t, rec) if _, present := body["displayname"]; present { t.Errorf("an unset display name must be omitted, got %s", rec.Body.String()) } rec = ts.do(request{method: http.MethodPut, path: profilePath + "/" + acc.UserID + "/displayname", token: acc.AccessToken, body: map[string]any{"displayname": "Alice"}}) expectStatus(t, rec, http.StatusOK) rec = ts.do(request{method: http.MethodPut, path: profilePath + "/" + acc.UserID + "/avatar_url", token: acc.AccessToken, body: map[string]any{"avatar_url": "mxc://warp.example/abc"}}) expectStatus(t, rec, http.StatusOK) rec = ts.get(profilePath+"/"+acc.UserID, "") expectStatus(t, rec, http.StatusOK) body = jsonBody(t, rec) if body["displayname"] != "Alice" || body["avatar_url"] != "mxc://warp.example/abc" { t.Errorf("profile = %s", rec.Body.String()) } // The per-field endpoints return only their own field. rec = ts.get(profilePath+"/"+acc.UserID+"/displayname", "") expectStatus(t, rec, http.StatusOK) body = jsonBody(t, rec) if body["displayname"] != "Alice" { t.Errorf("displayname = %v", body["displayname"]) } if _, present := body["avatar_url"]; present { t.Error("/displayname must not include the avatar") } rec = ts.get(profilePath+"/"+acc.UserID+"/avatar_url", "") expectStatus(t, rec, http.StatusOK) if jsonBody(t, rec)["avatar_url"] != "mxc://warp.example/abc" { t.Errorf("avatar_url = %s", rec.Body.String()) } } // TestProfileReadsArePublic covers the rule a member list depends on: a // client showing somebody's name has no token for them. func TestProfileReadsArePublic(t *testing.T) { ts := newTestServer(t, nil) acc := ts.register("alice", "superuser") rec := ts.do(request{method: http.MethodPut, path: profilePath + "/" + acc.UserID + "/displayname", token: acc.AccessToken, body: map[string]any{"displayname": "my_display_name"}}) expectStatus(t, rec, http.StatusOK) for _, path := range []string{ profilePath + "/" + acc.UserID, profilePath + "/" + acc.UserID + "/displayname", } { rec := ts.get(path, "") expectStatus(t, rec, http.StatusOK) if name := jsonBody(t, rec)["displayname"]; name != "my_display_name" { t.Errorf("GET %s without a token returned %v", path, name) } } } func TestProfileWritesAreOwnOnly(t *testing.T) { ts := newTestServer(t, nil) alice := ts.register("alice", "superuser") bob := ts.register("bob", "bobspassword") rec := ts.do(request{method: http.MethodPut, path: profilePath + "/" + alice.UserID + "/displayname", token: bob.AccessToken, body: map[string]any{"displayname": "not bob"}}) expectError(t, rec, http.StatusForbidden, "M_FORBIDDEN") // An admin may, which is what lets the panel clean up a profile. ts.makeAdmin(bob) rec = ts.do(request{method: http.MethodPut, path: profilePath + "/" + alice.UserID + "/displayname", token: bob.AccessToken, body: map[string]any{"displayname": "set by an admin"}}) expectStatus(t, rec, http.StatusOK) } func TestProfileWritesRequireAToken(t *testing.T) { ts := newTestServer(t, nil) acc := ts.register("alice", "superuser") rec := ts.do(request{method: http.MethodPut, path: profilePath + "/" + acc.UserID + "/displayname", body: map[string]any{"displayname": "Alice"}}) expectError(t, rec, http.StatusUnauthorized, "M_MISSING_TOKEN") } func TestProfileOfUnknownUserIs404(t *testing.T) { ts := newTestServer(t, nil) expectError(t, ts.get(profilePath+"/@nobody:warp.example", ""), http.StatusNotFound, "M_NOT_FOUND") } func TestProfileRejectsMalformedUserID(t *testing.T) { ts := newTestServer(t, nil) expectError(t, ts.get(profilePath+"/@no-server-name", ""), http.StatusBadRequest, "M_INVALID_PARAM") } // TestProfileOfRemoteUserHonoursTheFederationSwitch is where PLAN.md // section 3 shows up in the Client-Server API: with federation off there is // no way to answer, and the spec's M_FORBIDDEN with a clear message is the // honest reply. func TestProfileOfRemoteUserHonoursTheFederationSwitch(t *testing.T) { ts := newTestServer(t, nil) rec := ts.get(profilePath+"/@bob:other.example", "") expectError(t, rec, http.StatusForbidden, "M_FORBIDDEN") if msg, _ := jsonBody(t, rec)["error"].(string); !strings.Contains(msg, "Federation is disabled") { t.Errorf("the message should say why, got %q", msg) } // With the switch on it is no longer a refusal, only something that is // not implemented until Phase 5. if err := ts.settings.SetFederationEnabled(t.Context(), true); err != nil { t.Fatalf("SetFederationEnabled: %v", err) } expectError(t, ts.get(profilePath+"/@bob:other.example", ""), http.StatusNotFound, "M_NOT_FOUND") } func TestProfileRejectsOverlongDisplayName(t *testing.T) { ts := newTestServer(t, nil) acc := ts.register("alice", "superuser") rec := ts.do(request{method: http.MethodPut, path: profilePath + "/" + acc.UserID + "/displayname", token: acc.AccessToken, body: map[string]any{"displayname": strings.Repeat("x", 1000)}}) expectError(t, rec, http.StatusBadRequest, "M_TOO_LARGE") }