import { beforeEach, describe, expect, it, vi } from "vitest"; import { recordAuditLog } from "@/modules/core/server/audit"; import { requirePermission } from "@/modules/core/server/auth"; import type { AuthContext } from "@/modules/core/types"; import type { AlertCenterData, AlertRule } from "@/modules/alerts/types"; import type { DeviceAsset, DeviceOperationsData } from "@/modules/devices/types"; import type { FamilyContact, FamilyServiceData } from "@/modules/family/types"; import type { Notice } from "@/modules/notices/types"; import { createAlertRule, listAlertCenterData, updateAlertTrigger, } from "@/modules/alerts/server/operations"; import { createDeviceAsset, listDeviceOperationsData, updateMaintenanceTicket, } from "@/modules/devices/server/operations"; import { createFamilyContact, listFamilyServiceData, updateFamilyVisit, } from "@/modules/family/server/operations"; import { createNotice, listNoticeCenterData, markNoticeRead, } from "@/modules/notices/server/operations"; vi.mock("@/modules/core/server/auth", () => ({ requirePermission: vi.fn(), })); vi.mock("@/modules/core/server/audit", () => ({ recordAuditLog: vi.fn(), })); vi.mock("@/modules/devices/server/operations", () => ({ createDeviceAsset: vi.fn(), isDeviceMutationFailure: vi.fn((value: unknown) => typeof value === "object" && value !== null && "success" in value && value.success === false), listDeviceOperationsData: vi.fn(), updateMaintenanceTicket: vi.fn(), })); vi.mock("@/modules/notices/server/operations", () => ({ createNotice: vi.fn(), isNoticeMutationFailure: vi.fn((value: unknown) => typeof value === "object" && value !== null && "success" in value && value.success === false), listNoticeCenterData: vi.fn(), markNoticeRead: vi.fn(), })); vi.mock("@/modules/alerts/server/operations", () => ({ createAlertRule: vi.fn(), isAlertMutationFailure: vi.fn((value: unknown) => typeof value === "object" && value !== null && "success" in value && value.success === false), listAlertCenterData: vi.fn(), updateAlertTrigger: vi.fn(), })); vi.mock("@/modules/family/server/operations", () => ({ createFamilyContact: vi.fn(), isFamilyMutationFailure: vi.fn((value: unknown) => typeof value === "object" && value !== null && "success" in value && value.success === false), listFamilyServiceData: vi.fn(), updateFamilyVisit: vi.fn(), })); const account: AuthContext["account"] = { id: "account-1", name: "Admin", email: "admin@example.com", avatarUrl: "", role: "org_admin", status: "active", createdAt: "2026-07-02T00:00:00.000Z", updatedAt: "2026-07-02T00:00:00.000Z", }; function createAuthContext(overrides: Partial = {}): AuthContext { return { account, organization: { id: "org-1", name: "Demo Org", slug: "demo-org", status: "active", registrationEnabled: true, oidcEnabled: false, oidcIssuerUrl: "", oidcClientId: "", oidcHasClientSecret: false, oidcScopes: "openid profile email", oidcRedirectUri: "", oidcAvatarClaim: "picture", oidcAutoProvision: false, createdAt: "2026-07-02T00:00:00.000Z", updatedAt: "2026-07-02T00:00:00.000Z", }, organizations: [], permissions: ["device:read", "device:manage", "notice:read", "notice:manage", "alert:read", "alert:manage", "family:read", "family:manage"], session: { id: "session-1", accountId: "account-1", activeOrganizationId: "org-1", createdAt: "2026-07-02T00:00:00.000Z", expiresAt: "2026-07-09T00:00:00.000Z", }, ...overrides, }; } const device: DeviceAsset = { id: "device-1", organizationId: "org-1", name: "床头呼叫器", code: "DEV-1", category: "呼叫系统", location: "A102", status: "active", lastInspectedAt: undefined, notes: "", createdAt: "2026-07-02T00:00:00.000Z", updatedAt: "2026-07-02T00:00:00.000Z", }; const deviceData: DeviceOperationsData = { metrics: { activeDevices: 1, maintenanceDevices: 0, openTickets: 0, urgentTickets: 0 }, devices: [device], tickets: [], }; const notice: Notice = { id: "notice-1", organizationId: "org-1", title: "探访安排", content: "周六探访安排", audience: "全体员工", status: "published", publishedAt: "2026-07-02T00:00:00.000Z", createdByAccountId: "account-1", createdByName: "Admin", updatedByAccountId: "account-1", updatedByName: "Admin", readCount: 0, hasRead: false, createdAt: "2026-07-02T00:00:00.000Z", updatedAt: "2026-07-02T00:00:00.000Z", }; const alertRule: AlertRule = { id: "rule-1", organizationId: "org-1", name: "血氧低值连续告警", ruleType: "health", severity: "critical", status: "enabled", conditionSummary: "血氧低于阈值", suggestion: "通知医生", createdAt: "2026-07-02T00:00:00.000Z", updatedAt: "2026-07-02T00:00:00.000Z", }; const alertData: AlertCenterData = { metrics: { enabledRules: 1, openTriggers: 0, criticalTriggers: 0, resolvedTriggers: 0 }, rules: [alertRule], triggers: [], }; const familyContact: FamilyContact = { id: "contact-1", organizationId: "org-1", elderId: "elder-1", elderName: "王桂兰", name: "张敏", relationship: "女儿", phone: "13800000001", status: "active", notes: "", createdAt: "2026-07-02T00:00:00.000Z", updatedAt: "2026-07-02T00:00:00.000Z", }; const familyData: FamilyServiceData = { metrics: { activeContacts: 1, pendingVisits: 0, openFeedback: 0, resolvedFeedback: 0 }, elderOptions: [{ id: "elder-1", name: "王桂兰" }], contacts: [familyContact], visits: [], feedback: [], }; function allowAuth(overrides: Partial = {}): void { vi.mocked(requirePermission).mockResolvedValue({ success: true, context: createAuthContext(overrides) }); } function jsonRequest(body: Record): Request { return new Request("http://localhost/api/collaboration", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); } async function readJson(response: Response): Promise> { return (await response.json()) as Record; } beforeEach(() => { vi.clearAllMocks(); allowAuth(); }); describe("collaboration API routes", () => { it("loads device operations for the active organization", async () => { vi.mocked(listDeviceOperationsData).mockResolvedValue(deviceData); const { GET } = await import("./devices/assets/route"); const response = await GET(); const body = await readJson(response); expect(response.status).toBe(200); expect(body.data).toEqual(deviceData); expect(listDeviceOperationsData).toHaveBeenCalledWith("org-1"); }); it("requires active organization before loading notices", async () => { allowAuth({ organization: undefined }); const { GET } = await import("./notices/route"); const response = await GET(); const body = await readJson(response); expect(response.status).toBe(400); expect(body.reason).toBe("请选择机构后查看公告"); expect(listNoticeCenterData).not.toHaveBeenCalled(); }); it("creates a device asset and records audit", async () => { vi.mocked(createDeviceAsset).mockResolvedValue(device); const { POST } = await import("./devices/assets/route"); const response = await POST(jsonRequest({ name: "床头呼叫器", code: "DEV-1", status: "active" })); const body = await readJson(response); expect(response.status).toBe(201); expect(body.success).toBe(true); expect(createDeviceAsset).toHaveBeenCalledWith(expect.objectContaining({ organizationId: "org-1" })); expect(recordAuditLog).toHaveBeenCalledWith(expect.objectContaining({ action: "device.asset.create" })); }); it("updates a maintenance ticket and propagates cross-organization failures", async () => { vi.mocked(updateMaintenanceTicket).mockResolvedValue({ success: false, reason: "维修工单不存在", status: 404 }); const { PATCH } = await import("./devices/tickets/[id]/route"); const response = await PATCH(jsonRequest({ title: "保养", priority: "normal", status: "open" }), { params: Promise.resolve({ id: "ticket-from-other-org" }), }); const body = await readJson(response); expect(response.status).toBe(404); expect(body.reason).toBe("维修工单不存在"); expect(recordAuditLog).not.toHaveBeenCalled(); }); it("creates and marks notices read", async () => { vi.mocked(createNotice).mockResolvedValue(notice); vi.mocked(markNoticeRead).mockResolvedValue({ ...notice, hasRead: true, readCount: 1 }); const { POST } = await import("./notices/route"); const createResponse = await POST(jsonRequest({ title: "探访安排", content: "周六探访安排", status: "published" })); expect(createResponse.status).toBe(201); expect(recordAuditLog).toHaveBeenCalledWith(expect.objectContaining({ action: "notice.create" })); vi.clearAllMocks(); allowAuth(); const { POST: markReadPost } = await import("./notices/[id]/route"); const readResponse = await markReadPost(jsonRequest({}), { params: Promise.resolve({ id: "notice-1" }) }); expect(readResponse.status).toBe(200); expect(markNoticeRead).toHaveBeenCalledWith({ accountId: "account-1", id: "notice-1", organizationId: "org-1" }); }); it("loads and creates alert rules", async () => { vi.mocked(listAlertCenterData).mockResolvedValue(alertData); vi.mocked(createAlertRule).mockResolvedValue(alertRule); const { GET, POST } = await import("./alerts/rules/route"); const listResponse = await GET(); expect(listResponse.status).toBe(200); expect(listAlertCenterData).toHaveBeenCalledWith("org-1"); const createResponse = await POST(jsonRequest({ name: "血氧低值连续告警", ruleType: "health", severity: "critical", status: "enabled" })); expect(createResponse.status).toBe(201); expect(recordAuditLog).toHaveBeenCalledWith(expect.objectContaining({ action: "alert.rule.create" })); }); it("updates alert trigger failures without audit", async () => { vi.mocked(updateAlertTrigger).mockResolvedValue({ success: false, reason: "预警触发记录不存在", status: 404 }); const { PATCH } = await import("./alerts/triggers/[id]/route"); const response = await PATCH(jsonRequest({ title: "触发", status: "resolved" }), { params: Promise.resolve({ id: "trigger-other-org" }) }); const body = await readJson(response); expect(response.status).toBe(404); expect(body.reason).toBe("预警触发记录不存在"); expect(recordAuditLog).not.toHaveBeenCalled(); }); it("loads family services and creates contacts", async () => { vi.mocked(listFamilyServiceData).mockResolvedValue(familyData); vi.mocked(createFamilyContact).mockResolvedValue(familyContact); const { GET, POST } = await import("./family/contacts/route"); const listResponse = await GET(); expect(listResponse.status).toBe(200); expect(listFamilyServiceData).toHaveBeenCalledWith("org-1"); const createResponse = await POST( jsonRequest({ elderId: "elder-1", name: "张敏", relationship: "女儿", phone: "13800000001", status: "active" }), ); expect(createResponse.status).toBe(201); expect(recordAuditLog).toHaveBeenCalledWith(expect.objectContaining({ action: "family.contact.create" })); }); it("updates family visit failures without audit", async () => { vi.mocked(updateFamilyVisit).mockResolvedValue({ success: false, reason: "探访预约不存在", status: 404 }); const { PATCH } = await import("./family/visits/[id]/route"); const response = await PATCH( jsonRequest({ elderId: "elder-1", scheduledAt: "2026-07-02T08:00:00.000Z", status: "approved" }), { params: Promise.resolve({ id: "visit-other-org" }) }, ); const body = await readJson(response); expect(response.status).toBe(404); expect(body.reason).toBe("探访预约不存在"); expect(recordAuditLog).not.toHaveBeenCalled(); }); });