import { describe, expect, it, vi } from "vitest"; import { appRouter } from "./routers"; import type { TrpcContext } from "./_core/context"; type AuthenticatedUser = NonNullable; /* ─── Context helpers for each role ─── */ function createContext(role: "admin" | "agent" | "user" | null): TrpcContext { if (!role) { return { user: null, req: { protocol: "https", headers: {} } as TrpcContext["req"], res: { clearCookie: vi.fn() } as unknown as TrpcContext["res"], }; } const user: AuthenticatedUser = { id: role === "admin" ? 1 : role === "agent" ? 2 : 3, openId: `${role}-openid`, email: `${role}@homelegance.com`, name: `Test ${role.charAt(0).toUpperCase() + role.slice(1)}`, loginMethod: "manus", role, createdAt: new Date(), updatedAt: new Date(), lastSignedIn: new Date(), }; return { user, req: { protocol: "https", headers: {} } as TrpcContext["req"], res: { clearCookie: vi.fn() } as unknown as TrpcContext["res"], }; } /* ─── Mock database ─── */ vi.mock("./db", () => { const users = [ { id: 1, openId: "admin-openid", name: "Test Admin", email: "admin@homelegance.com", role: "admin", lastSignedIn: new Date(), createdAt: new Date(), updatedAt: new Date() }, { id: 2, openId: "agent-openid", name: "Test Agent", email: "agent@homelegance.com", role: "agent", lastSignedIn: new Date(), createdAt: new Date(), updatedAt: new Date() }, { id: 3, openId: "user-openid", name: "Test User", email: "user@homelegance.com", role: "user", lastSignedIn: new Date(), createdAt: new Date(), updatedAt: new Date() }, ]; return { createConversation: vi.fn(async (data: any) => ({ id: 1, ...data, createdAt: new Date(), updatedAt: new Date() })), getConversations: vi.fn(async () => []), getConversationById: vi.fn(async (id: number) => ({ id, sessionId: "test", status: "active", createdAt: new Date(), updatedAt: new Date() })), getConversationBySessionId: vi.fn(async () => null), updateConversationStatus: vi.fn(async (id: number, status: string) => ({ id, status })), getConversationStats: vi.fn(async () => ({ total: 5, active: 2, escalated: 1, resolved: 1, closed: 1 })), addMessage: vi.fn(async (data: any) => ({ id: 1, ...data, createdAt: new Date() })), getMessagesByConversation: vi.fn(async () => []), saveWorkflow: vi.fn(async (wid: string, nodes: any[], edges: any[]) => ({ workflowId: wid, nodeCount: nodes.length, edgeCount: edges.length })), getWorkflow: vi.fn(async () => ({ nodes: [], edges: [] })), getAllUsers: vi.fn(async () => users), updateUserRole: vi.fn(async (userId: number, role: string) => { const u = users.find(u => u.id === userId); if (!u) return null; return { ...u, role }; }), getUserById: vi.fn(async (userId: number) => users.find(u => u.id === userId)), getUserByEmail: vi.fn(async (email: string) => users.find(u => u.email === email) || null), deleteUser: vi.fn(async (userId: number) => users.find(u => u.id === userId) || null), createInvitation: vi.fn(async (data: any) => ({ id: 1, ...data, createdAt: new Date() })), getAllInvitations: vi.fn(async () => []), getInvitationByToken: vi.fn(async () => null), getInvitationByEmail: vi.fn(async () => []), updateInvitationStatus: vi.fn(async (id: number, status: string) => ({ id, status })), expireOldInvitations: vi.fn(async () => {}), createAuditLog: vi.fn(async (data: any) => ({ id: 1, ...data, createdAt: new Date() })), getAuditLogs: vi.fn(async () => []), getConversationsAdvanced: vi.fn(async () => ({ conversations: [], total: 0, page: 1, pageSize: 20, totalPages: 0 })), getConversationMessageCounts: vi.fn(async () => ({})), getAgentUsers: vi.fn(async () => []), bulkUpdateConversationStatus: vi.fn(async (ids: number[]) => ({ updated: ids.length })), deleteConversations: vi.fn(async (ids: number[]) => ({ deleted: ids.length })), getUserByEmailWithPassword: vi.fn(async () => null), createUserWithPassword: vi.fn(async (data: any) => ({ id: 10, ...data })), updateUserPassword: vi.fn(async () => {}), createPasswordResetToken: vi.fn(async (data: any) => ({ id: 1, ...data })), getPasswordResetToken: vi.fn(async () => null), markPasswordResetTokenUsed: vi.fn(async () => {}), upsertUser: vi.fn(), getUserByOpenId: vi.fn(), }; }); vi.mock("./_core/llm", () => ({ invokeLLM: vi.fn(async () => ({ id: "test", created: Date.now(), model: "test", choices: [{ index: 0, message: { role: "assistant", content: "Test response" }, finish_reason: "stop" }], })), })); /* ═══════════════════════════════════════════════════════════════ AGENT PROCEDURES — require agent or admin role ═══════════════════════════════════════════════════════════════ */ describe("Agent procedures — role-based access", () => { describe("Unauthenticated users", () => { it("rejects agent.stats for unauthenticated user", async () => { const caller = appRouter.createCaller(createContext(null)); await expect(caller.agent.stats()).rejects.toThrow(); }); it("rejects agent.conversations for unauthenticated user", async () => { const caller = appRouter.createCaller(createContext(null)); await expect(caller.agent.conversations({})).rejects.toThrow(); }); it("rejects agent.reply for unauthenticated user", async () => { const caller = appRouter.createCaller(createContext(null)); await expect(caller.agent.reply({ conversationId: 1, content: "Hi" })).rejects.toThrow(); }); }); describe("Regular users (role=user)", () => { it("rejects agent.stats for regular user", async () => { const caller = appRouter.createCaller(createContext("user")); await expect(caller.agent.stats()).rejects.toThrow("Agent or admin access required"); }); it("rejects agent.conversations for regular user", async () => { const caller = appRouter.createCaller(createContext("user")); await expect(caller.agent.conversations({})).rejects.toThrow("Agent or admin access required"); }); it("rejects agent.reply for regular user", async () => { const caller = appRouter.createCaller(createContext("user")); await expect(caller.agent.reply({ conversationId: 1, content: "Hi" })).rejects.toThrow("Agent or admin access required"); }); it("rejects agent.messages for regular user", async () => { const caller = appRouter.createCaller(createContext("user")); await expect(caller.agent.messages({ conversationId: 1 })).rejects.toThrow("Agent or admin access required"); }); it("rejects agent.updateStatus for regular user", async () => { const caller = appRouter.createCaller(createContext("user")); await expect(caller.agent.updateStatus({ conversationId: 1, status: "resolved" })).rejects.toThrow("Agent or admin access required"); }); }); describe("Agent users (role=agent)", () => { it("allows agent.stats for agent user", async () => { const caller = appRouter.createCaller(createContext("agent")); const stats = await caller.agent.stats(); expect(stats).toHaveProperty("total"); expect(stats).toHaveProperty("active"); expect(stats).toHaveProperty("escalated"); }); it("allows agent.conversations for agent user", async () => { const caller = appRouter.createCaller(createContext("agent")); const result = await caller.agent.conversations({}); expect(Array.isArray(result)).toBe(true); }); it("allows agent.messages for agent user", async () => { const caller = appRouter.createCaller(createContext("agent")); const result = await caller.agent.messages({ conversationId: 1 }); expect(Array.isArray(result)).toBe(true); }); it("allows agent.reply for agent user", async () => { const caller = appRouter.createCaller(createContext("agent")); const result = await caller.agent.reply({ conversationId: 1, content: "I can help you" }); expect(result).toHaveProperty("id"); expect(result.content).toBe("I can help you"); }); }); describe("Admin users (role=admin)", () => { it("allows agent.stats for admin user", async () => { const caller = appRouter.createCaller(createContext("admin")); const stats = await caller.agent.stats(); expect(stats.total).toBe(5); }); it("allows agent.conversations for admin user", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.agent.conversations({}); expect(Array.isArray(result)).toBe(true); }); }); }); /* ═══════════════════════════════════════════════════════════════ ADMIN PROCEDURES — require admin role only ═══════════════════════════════════════════════════════════════ */ describe("Admin procedures — role-based access", () => { describe("Users management", () => { it("rejects users.list for unauthenticated user", async () => { const caller = appRouter.createCaller(createContext(null)); await expect(caller.users.list()).rejects.toThrow(); }); it("rejects users.list for regular user", async () => { const caller = appRouter.createCaller(createContext("user")); await expect(caller.users.list()).rejects.toThrow(); }); it("rejects users.list for agent user", async () => { const caller = appRouter.createCaller(createContext("agent")); await expect(caller.users.list()).rejects.toThrow(); }); it("allows users.list for admin user", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.users.list(); expect(Array.isArray(result)).toBe(true); expect(result.length).toBe(3); }); it("allows users.updateRole for admin user", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.users.updateRole({ userId: 2, role: "admin" }); expect(result).toHaveProperty("role", "admin"); }); it("prevents admin from changing own role", async () => { const caller = appRouter.createCaller(createContext("admin")); await expect( caller.users.updateRole({ userId: 1, role: "user" }) ).rejects.toThrow("You cannot change your own role"); }); it("rejects users.updateRole for agent user", async () => { const caller = appRouter.createCaller(createContext("agent")); await expect( caller.users.updateRole({ userId: 3, role: "agent" }) ).rejects.toThrow(); }); it("allows users.getById for admin user", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.users.getById({ userId: 2 }); expect(result).toHaveProperty("name", "Test Agent"); }); }); describe("Workflow management", () => { it("rejects workflow.save for unauthenticated user", async () => { const caller = appRouter.createCaller(createContext(null)); await expect( caller.workflow.save({ workflowId: "test", nodes: [], edges: [] }) ).rejects.toThrow(); }); it("rejects workflow.save for regular user", async () => { const caller = appRouter.createCaller(createContext("user")); await expect( caller.workflow.save({ workflowId: "test", nodes: [], edges: [] }) ).rejects.toThrow(); }); it("rejects workflow.save for agent user", async () => { const caller = appRouter.createCaller(createContext("agent")); await expect( caller.workflow.save({ workflowId: "test", nodes: [], edges: [] }) ).rejects.toThrow(); }); it("allows workflow.save for admin user", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.workflow.save({ workflowId: "test", nodes: [{ workflowId: "test", nodeId: "n1", type: "greeting", label: "Welcome", config: {}, positionX: 100, positionY: 100, }], edges: [], }); expect(result.workflowId).toBe("test"); expect(result.nodeCount).toBe(1); }); it("rejects workflow.load for agent user", async () => { const caller = appRouter.createCaller(createContext("agent")); await expect( caller.workflow.load({ workflowId: "test" }) ).rejects.toThrow(); }); it("allows workflow.load for admin user", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.workflow.load({ workflowId: "test" }); expect(result).toHaveProperty("nodes"); expect(result).toHaveProperty("edges"); }); }); }); /* ═══════════════════════════════════════════════════════════════ PUBLIC PROCEDURES — accessible without auth ═══════════════════════════════════════════════════════════════ */ describe("Public procedures — no auth required", () => { it("allows auth.me without authentication", async () => { const caller = appRouter.createCaller(createContext(null)); const result = await caller.auth.me(); expect(result).toBeNull(); }); it("returns user for authenticated auth.me", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.auth.me(); expect(result).toHaveProperty("name", "Test Admin"); expect(result).toHaveProperty("role", "admin"); }); it("allows chat.getMessages without authentication", async () => { const caller = appRouter.createCaller(createContext(null)); const result = await caller.chat.getMessages({ sessionId: "test" }); expect(result).toHaveProperty("messages"); }); });