| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- import { describe, expect, it, vi } from "vitest";
- import { appRouter } from "./routers";
- import type { TrpcContext } from "./_core/context";
- type AuthenticatedUser = NonNullable<TrpcContext["user"]>;
- /* ─── Context helpers ─── */
- 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 ─── */
- const mockConversations = [
- { id: 1, sessionId: "s1", visitorName: "Alice", visitorEmail: "alice@test.com", status: "active", assignedAgentId: 2, metadata: {}, createdAt: new Date(), updatedAt: new Date() },
- { id: 2, sessionId: "s2", visitorName: "Bob", visitorEmail: "bob@test.com", status: "escalated", assignedAgentId: null, metadata: {}, createdAt: new Date(), updatedAt: new Date() },
- { id: 3, sessionId: "s3", visitorName: "Charlie", visitorEmail: null, status: "resolved", assignedAgentId: 1, metadata: {}, createdAt: new Date(), updatedAt: new Date() },
- ];
- const mockAgents = [
- { id: 1, name: "Admin User", email: "admin@homelegance.com", role: "admin" },
- { id: 2, name: "Agent User", email: "agent@homelegance.com", role: "agent" },
- ];
- 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 () => []),
- getConversationsAdvanced: vi.fn(async (params: any) => ({
- conversations: [
- { id: 1, sessionId: "s1", visitorName: "Alice", status: "active", assignedAgentId: 2, customerId: "CUST-001", salesRep: "Jane Smith", agentName: "Test Agent", createdAt: new Date(), updatedAt: new Date() },
- { id: 2, sessionId: "s2", visitorName: "Bob", status: "escalated", assignedAgentId: null, customerId: null, salesRep: null, agentName: null, createdAt: new Date(), updatedAt: new Date() },
- ],
- total: 25,
- page: params?.page || 1,
- pageSize: params?.pageSize || 20,
- totalPages: 2,
- })),
- 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: 25, active: 10, escalated: 5, resolved: 7, closed: 3 })),
- addMessage: vi.fn(async (data: any) => ({ id: 1, ...data, createdAt: new Date() })),
- getMessagesByConversation: vi.fn(async () => []),
- getConversationMessageCounts: vi.fn(async (ids: number[]) => {
- const counts: Record<number, number> = {};
- ids.forEach((id, i) => { counts[id] = (i + 1) * 3; });
- return counts;
- }),
- getAgentUsers: vi.fn(async () => [
- { id: 1, name: "Admin User", email: "admin@homelegance.com", role: "admin" },
- { id: 2, name: "Agent User", email: "agent@homelegance.com", role: "agent" },
- ]),
- bulkUpdateConversationStatus: vi.fn(async (ids: number[], status: string) => ({ updated: ids.length })),
- deleteConversations: vi.fn(async (ids: number[]) => ({ deleted: ids.length })),
- 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),
- 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 () => {}),
- 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 () => []),
- 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 from Ellie" }, finish_reason: "stop" }],
- })),
- }));
- /* ═══════════════════════════════════════════════════════════════
- ADVANCED CONVERSATION QUERIES
- ═══════════════════════════════════════════════════════════════ */
- describe("Advanced conversation queries", () => {
- it("returns paginated conversations with message counts for admin", async () => {
- const caller = appRouter.createCaller(createContext("admin"));
- const result = await caller.agent.conversationsAdvanced({
- page: 1,
- pageSize: 20,
- sortBy: "updated",
- sortOrder: "desc",
- });
- expect(result.total).toBe(25);
- expect(result.page).toBe(1);
- expect(result.totalPages).toBe(2);
- expect(result.conversations).toHaveLength(2);
- // Message counts should be enriched
- expect(result.conversations[0]).toHaveProperty("messageCount");
- expect(typeof result.conversations[0].messageCount).toBe("number");
- });
- it("returns paginated conversations for agent role", async () => {
- const caller = appRouter.createCaller(createContext("agent"));
- const result = await caller.agent.conversationsAdvanced({
- page: 1,
- pageSize: 20,
- sortBy: "updated",
- sortOrder: "desc",
- });
- expect(result.total).toBe(25);
- expect(result.conversations).toHaveLength(2);
- });
- it("rejects advanced query for regular user", async () => {
- const caller = appRouter.createCaller(createContext("user"));
- await expect(
- caller.agent.conversationsAdvanced({
- page: 1,
- pageSize: 20,
- sortBy: "updated",
- sortOrder: "desc",
- })
- ).rejects.toThrow("Agent or admin access required");
- });
- it("rejects advanced query for unauthenticated user", async () => {
- const caller = appRouter.createCaller(createContext(null));
- await expect(
- caller.agent.conversationsAdvanced({
- page: 1,
- pageSize: 20,
- sortBy: "updated",
- sortOrder: "desc",
- })
- ).rejects.toThrow();
- });
- it("supports filtering by status", async () => {
- const caller = appRouter.createCaller(createContext("admin"));
- const result = await caller.agent.conversationsAdvanced({
- page: 1,
- pageSize: 20,
- status: "escalated",
- sortBy: "updated",
- sortOrder: "desc",
- });
- expect(result).toBeDefined();
- expect(result.conversations).toBeDefined();
- });
- it("supports filtering by agent", async () => {
- const caller = appRouter.createCaller(createContext("admin"));
- const result = await caller.agent.conversationsAdvanced({
- page: 1,
- pageSize: 20,
- agentId: 2,
- sortBy: "updated",
- sortOrder: "desc",
- });
- expect(result).toBeDefined();
- });
- it("supports search query", async () => {
- const caller = appRouter.createCaller(createContext("agent"));
- const result = await caller.agent.conversationsAdvanced({
- page: 1,
- pageSize: 20,
- search: "Alice",
- sortBy: "updated",
- sortOrder: "desc",
- });
- expect(result).toBeDefined();
- });
- it("supports date range filtering", async () => {
- const caller = appRouter.createCaller(createContext("admin"));
- const result = await caller.agent.conversationsAdvanced({
- page: 1,
- pageSize: 20,
- dateFrom: "2025-01-01",
- dateTo: "2026-12-31",
- sortBy: "created",
- sortOrder: "asc",
- });
- expect(result).toBeDefined();
- });
- it("supports page 2 navigation", async () => {
- const caller = appRouter.createCaller(createContext("admin"));
- const result = await caller.agent.conversationsAdvanced({
- page: 2,
- pageSize: 20,
- sortBy: "updated",
- sortOrder: "desc",
- });
- expect(result.page).toBe(2);
- });
- it("supports sorting by customerId", async () => {
- const caller = appRouter.createCaller(createContext("admin"));
- const result = await caller.agent.conversationsAdvanced({
- page: 1,
- pageSize: 20,
- sortBy: "customerId",
- sortOrder: "asc",
- });
- expect(result).toBeDefined();
- expect(result.conversations).toBeDefined();
- });
- it("supports sorting by salesRep", async () => {
- const caller = appRouter.createCaller(createContext("agent"));
- const result = await caller.agent.conversationsAdvanced({
- page: 1,
- pageSize: 20,
- sortBy: "salesRep",
- sortOrder: "desc",
- });
- expect(result).toBeDefined();
- expect(result.conversations).toBeDefined();
- });
- it("supports sorting by agent name", async () => {
- const caller = appRouter.createCaller(createContext("admin"));
- const result = await caller.agent.conversationsAdvanced({
- page: 1,
- pageSize: 20,
- sortBy: "agent",
- sortOrder: "asc",
- });
- expect(result).toBeDefined();
- expect(result.conversations).toBeDefined();
- });
- it("returns agentName field in conversation data", async () => {
- const caller = appRouter.createCaller(createContext("admin"));
- const result = await caller.agent.conversationsAdvanced({
- page: 1,
- pageSize: 20,
- sortBy: "updated",
- sortOrder: "desc",
- });
- // First conversation has an assigned agent
- expect(result.conversations[0]).toHaveProperty("agentName");
- expect(result.conversations[0].agentName).toBe("Test Agent");
- // Second conversation has no assigned agent
- expect(result.conversations[1]).toHaveProperty("agentName");
- expect(result.conversations[1].agentName).toBeNull();
- });
- it("returns customerId and salesRep fields in conversation data", async () => {
- const caller = appRouter.createCaller(createContext("admin"));
- const result = await caller.agent.conversationsAdvanced({
- page: 1,
- pageSize: 20,
- sortBy: "updated",
- sortOrder: "desc",
- });
- expect(result.conversations[0]).toHaveProperty("customerId");
- expect(result.conversations[0].customerId).toBe("CUST-001");
- expect(result.conversations[0]).toHaveProperty("salesRep");
- expect(result.conversations[0].salesRep).toBe("Jane Smith");
- });
- });
- /* ═══════════════════════════════════════════════════════════════
- AGENT LIST
- ═══════════════════════════════════════════════════════════════ */
- describe("Agent list for filter dropdown", () => {
- it("returns agent list for admin", async () => {
- const caller = appRouter.createCaller(createContext("admin"));
- const agents = await caller.agent.agents();
- expect(agents).toHaveLength(2);
- expect(agents[0]).toHaveProperty("name");
- expect(agents[0]).toHaveProperty("role");
- });
- it("returns agent list for agent role", async () => {
- const caller = appRouter.createCaller(createContext("agent"));
- const agents = await caller.agent.agents();
- expect(agents).toHaveLength(2);
- });
- it("rejects agent list for regular user", async () => {
- const caller = appRouter.createCaller(createContext("user"));
- await expect(caller.agent.agents()).rejects.toThrow("Agent or admin access required");
- });
- });
- /* ═══════════════════════════════════════════════════════════════
- BULK ACTIONS
- ═══════════════════════════════════════════════════════════════ */
- describe("Bulk conversation actions", () => {
- it("allows agent to bulk resolve conversations", async () => {
- const caller = appRouter.createCaller(createContext("agent"));
- const result = await caller.agent.bulkUpdateStatus({
- conversationIds: [1, 2, 3],
- status: "resolved",
- });
- expect(result.updated).toBe(3);
- });
- it("allows admin to bulk close conversations", async () => {
- const caller = appRouter.createCaller(createContext("admin"));
- const result = await caller.agent.bulkUpdateStatus({
- conversationIds: [1, 2],
- status: "closed",
- });
- expect(result.updated).toBe(2);
- });
- it("rejects bulk action for regular user", async () => {
- const caller = appRouter.createCaller(createContext("user"));
- await expect(
- caller.agent.bulkUpdateStatus({ conversationIds: [1], status: "resolved" })
- ).rejects.toThrow("Agent or admin access required");
- });
- it("rejects bulk action for unauthenticated user", async () => {
- const caller = appRouter.createCaller(createContext(null));
- await expect(
- caller.agent.bulkUpdateStatus({ conversationIds: [1], status: "resolved" })
- ).rejects.toThrow();
- });
- });
- /* ═══════════════════════════════════════════════════════════════
- DELETE CONVERSATIONS (admin only)
- ═══════════════════════════════════════════════════════════════ */
- describe("Delete conversations (admin only)", () => {
- it("allows admin to delete conversations", async () => {
- const caller = appRouter.createCaller(createContext("admin"));
- const result = await caller.agent.deleteConversations({
- conversationIds: [1, 2],
- });
- expect(result.deleted).toBe(2);
- });
- it("rejects delete for agent role", async () => {
- const caller = appRouter.createCaller(createContext("agent"));
- await expect(
- caller.agent.deleteConversations({ conversationIds: [1] })
- ).rejects.toThrow();
- });
- it("rejects delete for regular user", async () => {
- const caller = appRouter.createCaller(createContext("user"));
- await expect(
- caller.agent.deleteConversations({ conversationIds: [1] })
- ).rejects.toThrow();
- });
- it("rejects delete for unauthenticated user", async () => {
- const caller = appRouter.createCaller(createContext(null));
- await expect(
- caller.agent.deleteConversations({ conversationIds: [1] })
- ).rejects.toThrow();
- });
- });
|