feat: avatar by link, loaded by the browser; Files nests workspaces under owners' homes
- POST /api/me/avatar/url stores the https link (users.avatar_link) instead of downloading it; GET /api/me/avatar and /api/users/:id/avatar answer 302 to it (no referrer). Link rules: https, no userinfo, <= 2048 chars, no localhost or private/reserved IP literal. Upload and link replace each other. - Settings -> Account first loads the link as an embedded image and refuses one this browser cannot show: a picture behind a login (Mattermost's /api/v4/users/<id>/image) gets no cookie cross-site and 401s. - Files sidebar: each workspace under its owner's home, chevrons remembered per browser (agenthub.files.homesOpen); ownerless ones stay flat.
This commit is contained in:
@@ -78,8 +78,40 @@ describe("AccountSettings", () => {
|
||||
expect(await screen.findByTestId("user-avatar-initials")).toBeTruthy();
|
||||
});
|
||||
|
||||
/** jsdom never loads an image: play the browser's answer to `src`. */
|
||||
const stubImage = (loads: boolean) =>
|
||||
vi.stubGlobal(
|
||||
"Image",
|
||||
class {
|
||||
naturalWidth = loads ? 64 : 0;
|
||||
referrerPolicy = "";
|
||||
onload: (() => void) | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
set src(_v: string) {
|
||||
setTimeout(() => (loads ? this.onload?.() : this.onerror?.()), 0);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
it("refuses a link this browser cannot load as an image, and saves nothing", async () => {
|
||||
const ue = userEvent.setup();
|
||||
stubImage(false);
|
||||
render(<AccountSettings />);
|
||||
await screen.findByTestId("account-card");
|
||||
await ue.click(screen.getByTestId("account-avatar-url-toggle"));
|
||||
await ue.type(
|
||||
screen.getByTestId("account-avatar-url-input"),
|
||||
"https://chat.example/api/v4/users/x/image{Enter}"
|
||||
);
|
||||
await waitFor(() => expect(screen.getByTestId("account-avatar-url-input")).toBeTruthy());
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
expect(me.setAvatarFromUrl).not.toHaveBeenCalled();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("sets the avatar from a link and closes the field", async () => {
|
||||
const ue = userEvent.setup();
|
||||
stubImage(true);
|
||||
const withAvatar = { ...account, user: { ...account.user, avatarAt: "2026-10-21T00:00:00Z" } };
|
||||
me.setAvatarFromUrl.mockResolvedValue(withAvatar);
|
||||
render(<AccountSettings />);
|
||||
@@ -92,6 +124,7 @@ describe("AccountSettings", () => {
|
||||
"/api/me/avatar?v=2026-10-21T00:00:00Z"
|
||||
);
|
||||
expect(screen.queryByTestId("account-avatar-url-input")).toBeNull();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("shows the account's own information", async () => {
|
||||
|
||||
Reference in New Issue
Block a user