mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 10:52:08 +01:00
feat: complete tRPC integration and fix platform UI issues
- Implement comprehensive tRPC setup with type-safe API - Create tRPC routers for dashboard, admin, and auth endpoints - Migrate frontend components to use tRPC client - Fix platform dashboard Settings button functionality - Add platform settings page with profile and security management - Create OpenAI API mocking infrastructure for cost-safe testing - Update tests to work with new tRPC architecture - Sync database schema to fix AIBatchRequest table errors
This commit is contained in:
@ -262,11 +262,14 @@ describe("Authentication API Routes", () => {
|
||||
resetTime: Date.now() + 60000,
|
||||
});
|
||||
|
||||
vi.mocked(InMemoryRateLimiter).mockImplementation(() => ({
|
||||
checkRateLimit: mockCheckRateLimit,
|
||||
cleanup: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
} as any));
|
||||
vi.mocked(InMemoryRateLimiter).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
checkRateLimit: mockCheckRateLimit,
|
||||
cleanup: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
}) as any
|
||||
);
|
||||
|
||||
const request = new NextRequest("http://localhost:3000/api/register", {
|
||||
method: "POST",
|
||||
|
||||
@ -340,4 +340,4 @@ describe("/api/dashboard/metrics", () => {
|
||||
expect(data.error).toBe("Internal server error");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { processUnprocessedSessions, getAIProcessingCosts } from "../../lib/processingScheduler";
|
||||
import {
|
||||
processUnprocessedSessions,
|
||||
getAIProcessingCosts,
|
||||
} from "../../lib/processingScheduler";
|
||||
|
||||
vi.mock("../../lib/prisma", () => ({
|
||||
prisma: {
|
||||
@ -85,7 +88,9 @@ describe("Processing Scheduler", () => {
|
||||
|
||||
it("should handle errors gracefully", async () => {
|
||||
const { prisma } = await import("../../lib/prisma");
|
||||
vi.mocked(prisma.session.findMany).mockRejectedValue(new Error("Database error"));
|
||||
vi.mocked(prisma.session.findMany).mockRejectedValue(
|
||||
new Error("Database error")
|
||||
);
|
||||
|
||||
await expect(processUnprocessedSessions(1)).resolves.not.toThrow();
|
||||
});
|
||||
@ -95,7 +100,7 @@ describe("Processing Scheduler", () => {
|
||||
it("should calculate processing costs correctly", async () => {
|
||||
const mockAggregation = {
|
||||
_sum: {
|
||||
totalCostEur: 10.50,
|
||||
totalCostEur: 10.5,
|
||||
promptTokens: 1000,
|
||||
completionTokens: 500,
|
||||
totalTokens: 1500,
|
||||
@ -106,12 +111,14 @@ describe("Processing Scheduler", () => {
|
||||
};
|
||||
|
||||
const { prisma } = await import("../../lib/prisma");
|
||||
vi.mocked(prisma.aIProcessingRequest.aggregate).mockResolvedValue(mockAggregation);
|
||||
vi.mocked(prisma.aIProcessingRequest.aggregate).mockResolvedValue(
|
||||
mockAggregation
|
||||
);
|
||||
|
||||
const result = await getAIProcessingCosts();
|
||||
|
||||
expect(result).toEqual({
|
||||
totalCostEur: 10.50,
|
||||
totalCostEur: 10.5,
|
||||
totalRequests: 25,
|
||||
totalPromptTokens: 1000,
|
||||
totalCompletionTokens: 500,
|
||||
@ -133,7 +140,9 @@ describe("Processing Scheduler", () => {
|
||||
};
|
||||
|
||||
const { prisma } = await import("../../lib/prisma");
|
||||
vi.mocked(prisma.aIProcessingRequest.aggregate).mockResolvedValue(mockAggregation);
|
||||
vi.mocked(prisma.aIProcessingRequest.aggregate).mockResolvedValue(
|
||||
mockAggregation
|
||||
);
|
||||
|
||||
const result = await getAIProcessingCosts();
|
||||
|
||||
@ -146,4 +155,4 @@ describe("Processing Scheduler", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,8 +2,8 @@ import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { parseTranscriptToMessages } from "../../lib/transcriptParser";
|
||||
|
||||
describe("Transcript Parser", () => {
|
||||
const startTime = new Date('2024-01-01T10:00:00Z');
|
||||
const endTime = new Date('2024-01-01T10:30:00Z');
|
||||
const startTime = new Date("2024-01-01T10:00:00Z");
|
||||
const endTime = new Date("2024-01-01T10:30:00Z");
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@ -22,7 +22,9 @@ describe("Transcript Parser", () => {
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.messages).toHaveLength(3);
|
||||
expect(result.messages![0].role).toBe("User");
|
||||
expect(result.messages![0].content).toBe("Hello, I need help with my account");
|
||||
expect(result.messages![0].content).toBe(
|
||||
"Hello, I need help with my account"
|
||||
);
|
||||
expect(result.messages![1].role).toBe("Assistant");
|
||||
expect(result.messages![2].role).toBe("User");
|
||||
expect(result.messages![2].content).toBe("I can't log in to my account");
|
||||
@ -42,7 +44,9 @@ User: I need support with my order
|
||||
expect(result.messages![0].role).toBe("User");
|
||||
expect(result.messages![0].content).toBe("Hello there");
|
||||
expect(result.messages![1].role).toBe("Assistant");
|
||||
expect(result.messages![1].content).toBe("Hello! How can I help you today?");
|
||||
expect(result.messages![1].content).toBe(
|
||||
"Hello! How can I help you today?"
|
||||
);
|
||||
expect(result.messages![2].role).toBe("User");
|
||||
expect(result.messages![2].content).toBe("I need support with my order");
|
||||
});
|
||||
@ -124,15 +128,17 @@ User: Third
|
||||
it("should handle empty content", () => {
|
||||
expect(parseTranscriptToMessages("", startTime, endTime)).toEqual({
|
||||
success: false,
|
||||
error: "Empty transcript content"
|
||||
error: "Empty transcript content",
|
||||
});
|
||||
expect(parseTranscriptToMessages(" \n\n ", startTime, endTime)).toEqual({
|
||||
expect(
|
||||
parseTranscriptToMessages(" \n\n ", startTime, endTime)
|
||||
).toEqual({
|
||||
success: false,
|
||||
error: "Empty transcript content"
|
||||
error: "Empty transcript content",
|
||||
});
|
||||
expect(parseTranscriptToMessages("\t\r\n", startTime, endTime)).toEqual({
|
||||
success: false,
|
||||
error: "Empty transcript content"
|
||||
error: "Empty transcript content",
|
||||
});
|
||||
});
|
||||
|
||||
@ -185,4 +191,4 @@ System: Mixed case system
|
||||
expect(firstTimestamp.getSeconds()).toBe(45);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -77,38 +77,48 @@ describe("Dashboard Components", () => {
|
||||
|
||||
it("should render chart with questions data", () => {
|
||||
render(<TopQuestionsChart data={mockQuestions} />);
|
||||
|
||||
|
||||
expect(screen.getByTestId("card")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("card-title")).toHaveTextContent("Top 5 Asked Questions");
|
||||
expect(screen.getByText("How do I reset my password?")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("card-title")).toHaveTextContent(
|
||||
"Top 5 Asked Questions"
|
||||
);
|
||||
expect(
|
||||
screen.getByText("How do I reset my password?")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with custom title", () => {
|
||||
render(<TopQuestionsChart data={mockQuestions} title="Custom Title" />);
|
||||
|
||||
expect(screen.getByTestId("card-title")).toHaveTextContent("Custom Title");
|
||||
|
||||
expect(screen.getByTestId("card-title")).toHaveTextContent(
|
||||
"Custom Title"
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle empty questions data", () => {
|
||||
render(<TopQuestionsChart data={[]} />);
|
||||
|
||||
|
||||
expect(screen.getByTestId("card")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("card-title")).toHaveTextContent("Top 5 Asked Questions");
|
||||
expect(screen.getByText("No questions data available")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("card-title")).toHaveTextContent(
|
||||
"Top 5 Asked Questions"
|
||||
);
|
||||
expect(
|
||||
screen.getByText("No questions data available")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display question counts as badges", () => {
|
||||
render(<TopQuestionsChart data={mockQuestions} />);
|
||||
|
||||
|
||||
expect(screen.getByText("25")).toBeInTheDocument();
|
||||
expect(screen.getByText("20")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show all questions with progress bars", () => {
|
||||
render(<TopQuestionsChart data={mockQuestions} />);
|
||||
|
||||
|
||||
// All questions should be rendered
|
||||
mockQuestions.forEach(question => {
|
||||
mockQuestions.forEach((question) => {
|
||||
expect(screen.getByText(question.question)).toBeInTheDocument();
|
||||
expect(screen.getByText(question.count.toString())).toBeInTheDocument();
|
||||
});
|
||||
@ -116,7 +126,7 @@ describe("Dashboard Components", () => {
|
||||
|
||||
it("should calculate and display total questions", () => {
|
||||
render(<TopQuestionsChart data={mockQuestions} />);
|
||||
|
||||
|
||||
const totalQuestions = mockQuestions.reduce((sum, q) => sum + q.count, 0);
|
||||
expect(screen.getByText(totalQuestions.toString())).toBeInTheDocument();
|
||||
expect(screen.getByText("Total questions analyzed")).toBeInTheDocument();
|
||||
@ -133,71 +143,75 @@ Assistant: Let me help you with that. Can you tell me what error message you're
|
||||
|
||||
it("should render transcript content", () => {
|
||||
render(
|
||||
<TranscriptViewer
|
||||
<TranscriptViewer
|
||||
transcriptContent={mockTranscriptContent}
|
||||
transcriptUrl={mockTranscriptUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
expect(screen.getByText("Session Transcript")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Hello, I need help with my account/)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Hello, I need help with my account/)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle empty transcript content", () => {
|
||||
render(
|
||||
<TranscriptViewer
|
||||
<TranscriptViewer
|
||||
transcriptContent=""
|
||||
transcriptUrl={mockTranscriptUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("No transcript content available.")).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText("No transcript content available.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render without transcript URL", () => {
|
||||
render(
|
||||
<TranscriptViewer
|
||||
transcriptContent={mockTranscriptContent}
|
||||
/>
|
||||
);
|
||||
|
||||
render(<TranscriptViewer transcriptContent={mockTranscriptContent} />);
|
||||
|
||||
// Should still render content
|
||||
expect(screen.getByText("Session Transcript")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Hello, I need help with my account/)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Hello, I need help with my account/)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should toggle between formatted and raw view", () => {
|
||||
render(
|
||||
<TranscriptViewer
|
||||
<TranscriptViewer
|
||||
transcriptContent={mockTranscriptContent}
|
||||
transcriptUrl={mockTranscriptUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
// Find the raw text toggle button
|
||||
const rawToggleButton = screen.getByText("Raw Text");
|
||||
expect(rawToggleButton).toBeInTheDocument();
|
||||
|
||||
|
||||
// Click to show raw view
|
||||
fireEvent.click(rawToggleButton);
|
||||
|
||||
|
||||
// Should now show "Formatted" button and raw content
|
||||
expect(screen.getByText("Formatted")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle malformed transcript content gracefully", () => {
|
||||
const malformedContent = "This is not a properly formatted transcript";
|
||||
|
||||
|
||||
render(
|
||||
<TranscriptViewer
|
||||
<TranscriptViewer
|
||||
transcriptContent={malformedContent}
|
||||
transcriptUrl={mockTranscriptUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
// Should show "No transcript content available" in formatted view for malformed content
|
||||
expect(screen.getByText("No transcript content available.")).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText("No transcript content available.")
|
||||
).toBeInTheDocument();
|
||||
|
||||
// But should show the raw content when toggled to raw view
|
||||
const rawToggleButton = screen.getByText("Raw Text");
|
||||
fireEvent.click(rawToggleButton);
|
||||
@ -206,28 +220,30 @@ Assistant: Let me help you with that. Can you tell me what error message you're
|
||||
|
||||
it("should parse and display conversation messages", () => {
|
||||
render(
|
||||
<TranscriptViewer
|
||||
<TranscriptViewer
|
||||
transcriptContent={mockTranscriptContent}
|
||||
transcriptUrl={mockTranscriptUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
// Check for message content
|
||||
expect(screen.getByText(/Hello, I need help with my account/)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Hello, I need help with my account/)
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/I'd be happy to help you/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display transcript URL link when provided", () => {
|
||||
render(
|
||||
<TranscriptViewer
|
||||
<TranscriptViewer
|
||||
transcriptContent={mockTranscriptContent}
|
||||
transcriptUrl={mockTranscriptUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
const link = screen.getByText("View Full Raw");
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link.closest("a")).toHaveAttribute("href", mockTranscriptUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { InMemoryRateLimiter, extractClientIP } from "../../lib/rateLimiter";
|
||||
import { validateInput, registerSchema, loginSchema, forgotPasswordSchema } from "../../lib/validation";
|
||||
import {
|
||||
validateInput,
|
||||
registerSchema,
|
||||
loginSchema,
|
||||
forgotPasswordSchema,
|
||||
} from "../../lib/validation";
|
||||
import { z } from "zod";
|
||||
|
||||
// Import password schema directly from validation file
|
||||
@ -63,7 +68,7 @@ describe("Security Tests", () => {
|
||||
expect(rateLimiter.checkRateLimit("test-ip").allowed).toBe(false);
|
||||
|
||||
// Wait for window to expire
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
await new Promise((resolve) => setTimeout(resolve, 1100));
|
||||
|
||||
// Should be allowed again
|
||||
expect(rateLimiter.checkRateLimit("test-ip").allowed).toBe(true);
|
||||
@ -89,7 +94,7 @@ describe("Security Tests", () => {
|
||||
}
|
||||
|
||||
// Wait for entries to expire
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
await new Promise((resolve) => setTimeout(resolve, 1100));
|
||||
|
||||
// Force cleanup by checking rate limit
|
||||
rateLimiter.checkRateLimit("cleanup-trigger");
|
||||
@ -157,13 +162,13 @@ describe("Security Tests", () => {
|
||||
const weakPasswords = [
|
||||
"short", // Too short
|
||||
"nouppercase123!", // No uppercase
|
||||
"NOLOWERCASE123!", // No lowercase
|
||||
"NOLOWERCASE123!", // No lowercase
|
||||
"NoNumbers!@#", // No numbers
|
||||
"NoSpecialChars123", // No special chars
|
||||
"password123!", // Common password pattern
|
||||
];
|
||||
|
||||
weakPasswords.forEach(password => {
|
||||
weakPasswords.forEach((password) => {
|
||||
const result = validateInput(passwordSchema, password);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
@ -176,7 +181,7 @@ describe("Security Tests", () => {
|
||||
"MyS3cur3P@ssword!",
|
||||
];
|
||||
|
||||
strongPasswords.forEach(password => {
|
||||
strongPasswords.forEach((password) => {
|
||||
const result = validateInput(passwordSchema, password);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
@ -302,4 +307,4 @@ describe("Security Tests", () => {
|
||||
expect(true).toBe(true); // Placeholder for cookie config tests
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user