import { afterEach, describe, expect, it, vi } from "vitest"; import { addOrganizationMemberAndCreatePlatformUser, createOrganizationGroup, getOrganizationApiConfig, isOrganizationPermissionDenied, isOrganizationRouteNotFound, listOrganizationMembers, resetPlatformUserPassword } from "@/lib/server/organization-client"; describe("organization account client", () => { afterEach(() => { vi.unstubAllEnvs(); vi.unstubAllGlobals(); }); it("tracks required organization and staff API configuration separately", () => { vi.stubEnv("ZHINIAN_ORG_API_BASE_URL", "https://gateway.example.com/basic"); vi.stubEnv("ZHINIAN_ORG_API_TOKEN", "org-token"); expect(getOrganizationApiConfig()).toMatchObject({ configured: true, staffConfigured: false, staffMissing: ["ZHINIAN_STAFF_API_BASE_URL", "ZHINIAN_ORG_TENANT_ID"] }); }); it("accepts the current login token instead of a static organization token", () => { vi.stubEnv("ZHINIAN_ORG_API_BASE_URL", "https://gateway.example.com/basic"); vi.stubEnv("ZHINIAN_STAFF_API_BASE_URL", "https://gateway.example.com/staff"); vi.stubEnv("ZHINIAN_ORG_TENANT_ID", "1"); expect(getOrganizationApiConfig("login-token")).toMatchObject({ organizationToken: "login-token", staffToken: "login-token", configured: true, staffConfigured: true, missing: [], staffMissing: [] }); }); it("calls member list through the hotelStaff admin organization proxy by default", async () => { const seen: Array<{ input: string; init?: RequestInit }> = []; stubOrganizationEnv(); vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { seen.push({ input: String(input), init }); return jsonResponse({ code: 0, data: { records: [], total: 0, size: 10, current: 1, pages: 0 } }); }); await listOrganizationMembers({ organizationId: "org_1", pageNum: 1, pageSize: 10 }); expect(seen[0].input).toBe("https://gateway.example.com/basic/adminOrganization/organizationMember/organizationMemberList"); expect(seen[0].init?.method).toBe("POST"); expect(seen[0].init?.headers).toMatchObject({ Authorization: "Bearer org-token", tenantId: "1" }); expect(seen[0].init?.headers).not.toMatchObject({ from: "Y" }); }); it("prefers the current login token when forwarding organization requests", async () => { const seen: Array<{ input: string; init?: RequestInit }> = []; stubOrganizationEnv(); vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { seen.push({ input: String(input), init }); return jsonResponse({ code: 0, data: { records: [], total: 0, size: 10, current: 1, pages: 0 } }); }); await listOrganizationMembers( { organizationId: "org_1", pageNum: 1, pageSize: 10 }, { accessToken: "login-token" } ); expect(seen[0].init?.headers).toMatchObject({ Authorization: "Bearer login-token", tenantId: "1" }); }); it("creates organization groups through the documented group endpoint", async () => { const seen: Array<{ input: string; init?: RequestInit }> = []; stubOrganizationEnv(); vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { seen.push({ input: String(input), init }); return jsonResponse({ code: 0, data: true }); }); await createOrganizationGroup({ organizationId: "org_1", groupName: "客房部", groupDesc: "处理客房相关工单" }, { accessToken: "login-token" }); expect(seen[0].input).toBe("https://gateway.example.com/basic/organizationGroup/createOrganizationGroup"); expect(seen[0].init?.headers).toMatchObject({ Authorization: "Bearer login-token", tenantId: "1" }); expect(JSON.parse(String(seen[0].init?.body))).toMatchObject({ organizationId: "org_1", groupName: "客房部", groupDesc: "处理客房相关工单" }); }); it("allows overriding the member list path exposed by the gateway", async () => { const seen: Array<{ input: string; init?: RequestInit }> = []; stubOrganizationEnv(); vi.stubEnv("ZHINIAN_ORG_MEMBER_LIST_PATH", "/adminOrganization/organizationMember/list"); vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { seen.push({ input: String(input), init }); return jsonResponse({ code: 0, data: { records: [], total: 0, size: 10, current: 1, pages: 0 } }); }); await listOrganizationMembers({ organizationId: "org_1", pageNum: 1, pageSize: 10 }); expect(seen[0].input).toBe("https://gateway.example.com/basic/adminOrganization/organizationMember/list"); expect(seen[0].init?.headers).not.toMatchObject({ from: "Y" }); }); it("keeps the inner marker when the member list path points directly at the basic service", async () => { const seen: Array<{ input: string; init?: RequestInit }> = []; stubOrganizationEnv(); vi.stubEnv("ZHINIAN_ORG_MEMBER_LIST_PATH", "/organizationMember/organizationMemberList"); vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { seen.push({ input: String(input), init }); return jsonResponse({ code: 0, data: { records: [], total: 0, size: 10, current: 1, pages: 0 } }); }); await listOrganizationMembers({ organizationId: "org_1", pageNum: 1, pageSize: 10 }); expect(seen[0].input).toBe("https://gateway.example.com/basic/organizationMember/organizationMemberList"); expect(seen[0].init?.headers).toMatchObject({ from: "Y" }); }); it("marks static resource errors as missing organization routes", async () => { stubOrganizationEnv(); vi.stubGlobal("fetch", async () => new Response(JSON.stringify({ code: 1, msg: "No static resource organizationMember/organizationMemberList." }), { status: 404, headers: { "Content-Type": "application/json" } })); try { await listOrganizationMembers({ organizationId: "org_1", pageNum: 1, pageSize: 10 }); throw new Error("expected organization request to fail"); } catch (error) { expect(isOrganizationRouteNotFound(error)).toBe(true); expect(error).toMatchObject({ status: 404, message: expect.stringContaining("ZHINIAN_ORG_MEMBER_LIST_PATH") }); } }); it("marks upstream administrator-role errors as organization permission denials", async () => { stubOrganizationEnv(); vi.stubGlobal("fetch", async () => jsonResponse({ code: 1, msg: "仅管理员角色允许调用" })); try { await listOrganizationMembers({ organizationId: "org_1", pageNum: 1, pageSize: 10 }); throw new Error("expected organization request to fail"); } catch (error) { expect(isOrganizationPermissionDenied(error)).toBe(true); expect(isOrganizationRouteNotFound(error)).toBe(false); expect(error).toMatchObject({ status: 502, message: "仅管理员角色允许调用" }); } }); it("uses staff service endpoints for user creation and password reset", async () => { const seen: Array<{ input: string; init?: RequestInit }> = []; stubOrganizationEnv(); vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { seen.push({ input: String(input), init }); if (String(input).endsWith("/resetPlatformUserPassword")) return jsonResponse({ code: 0, data: true }); return jsonResponse({ code: 0, data: { userId: "10001", username: "zhangsan", phone: "13800000000", tenantId: 1, initialPassword: "Nianxx@123456" } }); }); const user = await addOrganizationMemberAndCreatePlatformUser({ tenantId: 1, username: "zhangsan", phone: "13800000000", name: "张三", memberName: "张三", memberPhone: "13800000000", roleId: "role_1", organizationId: "org_1", groupId: "group_1", mustChangePassword: true }); await resetPlatformUserPassword({ tenantId: 1, userId: 10001, newPassword: "Nianxx@654321", mustChangePassword: true }); expect(user.initialPassword).toBe("Nianxx@123456"); expect(seen[0].input).toBe("https://gateway.example.com/staff/adminOrganization/organizationMember/addOrganizationMemberAndCreatePlatformUser"); expect(seen[1].input).toBe("https://gateway.example.com/staff/adminPcUser/resetPlatformUserPassword"); expect(seen[0].init?.headers).toMatchObject({ Authorization: "Bearer staff-token", tenantId: "1" }); expect(JSON.parse(String(seen[1].init?.body))).toMatchObject({ tenantId: 1, userId: 10001, newPassword: "Nianxx@654321", mustChangePassword: true }); }); }); function stubOrganizationEnv() { vi.stubEnv("ZHINIAN_ORG_API_BASE_URL", "https://gateway.example.com/basic"); vi.stubEnv("ZHINIAN_ORG_API_TOKEN", "org-token"); vi.stubEnv("ZHINIAN_STAFF_API_BASE_URL", "https://gateway.example.com/staff"); vi.stubEnv("ZHINIAN_STAFF_API_TOKEN", "staff-token"); vi.stubEnv("ZHINIAN_ORG_TENANT_ID", "1"); } function jsonResponse(value: unknown) { return new Response(JSON.stringify(value), { status: 200, headers: { "Content-Type": "application/json" } }); }