mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* fix(frontend): stop re-validating account emails the backend accepted userSchema checked email with zod's email(), which rejects many addresses pydantic EmailStr registers (a&b@example.com, a@b.c, Unicode). /auth/me for such an account parsed as malformed: SSR answered gateway_unavailable, the offline banner never recovered, and the admin user list failed for every administrator. The backend owns address validation; keep a non-empty string. * docs(changelog): reference #7449 for the account email schema fix
158 lines
4.3 KiB
TypeScript
158 lines
4.3 KiB
TypeScript
import {
|
|
afterEach,
|
|
beforeEach,
|
|
describe,
|
|
expect,
|
|
test,
|
|
rs,
|
|
} from "@rstest/core";
|
|
|
|
import { GatewayApiError } from "@/core/api/errors";
|
|
import { loadAdminUsers, updateUserAccount } from "@/core/auth/admin-users";
|
|
|
|
const jsonResponse = (body: unknown, status = 200) =>
|
|
new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
|
|
describe("admin user management client", () => {
|
|
let originalFetch: typeof globalThis.fetch;
|
|
|
|
beforeEach(() => {
|
|
originalFetch = globalThis.fetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
|
|
test("loadAdminUsers requests the admin list endpoint", async () => {
|
|
const fetchSpy = rs.fn(async () =>
|
|
jsonResponse([
|
|
{
|
|
id: "u-1",
|
|
email: "a@example.com",
|
|
system_role: "admin",
|
|
needs_setup: false,
|
|
oauth_provider: null,
|
|
disabled: true,
|
|
},
|
|
]),
|
|
);
|
|
globalThis.fetch = fetchSpy as unknown as typeof globalThis.fetch;
|
|
|
|
const users = await loadAdminUsers();
|
|
|
|
expect(fetchSpy).toHaveBeenCalledWith(
|
|
"/api/v1/admin/users",
|
|
expect.objectContaining({ method: "GET" }),
|
|
);
|
|
expect(users).toEqual([
|
|
{
|
|
id: "u-1",
|
|
email: "a@example.com",
|
|
system_role: "admin",
|
|
needs_setup: false,
|
|
oauth_provider: null,
|
|
disabled: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("updateUserAccount PATCHes a JSON body with only the changed fields", async () => {
|
|
const fetchSpy = rs.fn(async () =>
|
|
jsonResponse({
|
|
id: "u-1",
|
|
email: "a@example.com",
|
|
system_role: "admin",
|
|
needs_setup: false,
|
|
oauth_provider: null,
|
|
disabled: true,
|
|
}),
|
|
);
|
|
globalThis.fetch = fetchSpy as unknown as typeof globalThis.fetch;
|
|
|
|
await updateUserAccount("u 1/id", { disabled: true });
|
|
|
|
const [url, init] = rs.mocked(fetchSpy).mock.calls[0] as unknown as [
|
|
string,
|
|
RequestInit & { body: string },
|
|
];
|
|
expect(url).toBe("/api/v1/admin/users/u%201%2Fid");
|
|
expect(init.method).toBe("PATCH");
|
|
expect(init.headers).toMatchObject({ "Content-Type": "application/json" });
|
|
expect(JSON.parse(init.body)).toEqual({ disabled: true });
|
|
});
|
|
|
|
test("a 409 conflict surfaces the gateway detail as the error message", async () => {
|
|
globalThis.fetch = rs.fn(async () =>
|
|
jsonResponse(
|
|
{ detail: "cannot disable the last remaining active admin" },
|
|
409,
|
|
),
|
|
) as unknown as typeof globalThis.fetch;
|
|
|
|
const error = await updateUserAccount("u-1", { disabled: true }).catch(
|
|
(err: unknown) => err,
|
|
);
|
|
|
|
expect(error).toBeInstanceOf(GatewayApiError);
|
|
expect(error).toMatchObject({
|
|
status: 409,
|
|
message: "cannot disable the last remaining active admin",
|
|
});
|
|
});
|
|
|
|
test("a coded-envelope 409 still surfaces its message verbatim", async () => {
|
|
globalThis.fetch = rs.fn(async () =>
|
|
jsonResponse(
|
|
{
|
|
detail: {
|
|
code: "last_active_admin",
|
|
message: "cannot disable the last remaining active admin",
|
|
},
|
|
},
|
|
409,
|
|
),
|
|
) as unknown as typeof globalThis.fetch;
|
|
|
|
const error = await updateUserAccount("u-1", { disabled: true }).catch(
|
|
(err: unknown) => err,
|
|
);
|
|
|
|
expect(error).toBeInstanceOf(GatewayApiError);
|
|
expect(error).toMatchObject({
|
|
status: 409,
|
|
message: "cannot disable the last remaining active admin",
|
|
});
|
|
});
|
|
|
|
test("lists users whose addresses only the backend's validator accepts", async () => {
|
|
// One such account used to fail the whole list for every administrator.
|
|
const rows = ["a&b@example.com", "a@b.c", "josé@example.com"].map(
|
|
(email, index) => ({
|
|
id: `u-${index}`,
|
|
email,
|
|
system_role: "user",
|
|
needs_setup: false,
|
|
oauth_provider: null,
|
|
disabled: false,
|
|
}),
|
|
);
|
|
globalThis.fetch = rs.fn(async () =>
|
|
jsonResponse(rows),
|
|
) as unknown as typeof globalThis.fetch;
|
|
|
|
await expect(loadAdminUsers()).resolves.toEqual(rows);
|
|
});
|
|
|
|
test("a malformed row rejects instead of rendering a phantom user", async () => {
|
|
globalThis.fetch = rs.fn(async () =>
|
|
jsonResponse([{ email: "no-id@example.com" }]),
|
|
) as unknown as typeof globalThis.fetch;
|
|
|
|
await expect(loadAdminUsers()).rejects.toThrow();
|
|
});
|
|
});
|