mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 12:12:09 +01:00
- 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
488 lines
14 KiB
TypeScript
488 lines
14 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
import { POST as registerPOST } from "../../app/api/register/route";
|
|
import { POST as forgotPasswordPOST } from "../../app/api/forgot-password/route";
|
|
import { NextRequest } from "next/server";
|
|
|
|
// Mock bcrypt
|
|
vi.mock("bcryptjs", () => ({
|
|
default: {
|
|
hash: vi.fn().mockResolvedValue("hashed-password"),
|
|
compare: vi.fn().mockResolvedValue(true),
|
|
},
|
|
}));
|
|
|
|
// Mock crypto
|
|
vi.mock("node:crypto", () => ({
|
|
default: {
|
|
randomBytes: vi.fn().mockReturnValue({
|
|
toString: vi.fn().mockReturnValue("random-token"),
|
|
}),
|
|
},
|
|
}));
|
|
|
|
// Mock prisma
|
|
vi.mock("../../lib/prisma", () => ({
|
|
prisma: {
|
|
user: {
|
|
findUnique: vi.fn(),
|
|
create: vi.fn(),
|
|
update: vi.fn(),
|
|
},
|
|
company: {
|
|
findUnique: vi.fn(),
|
|
},
|
|
},
|
|
}));
|
|
|
|
// Mock email service
|
|
vi.mock("../../lib/sendEmail", () => ({
|
|
sendEmail: vi.fn().mockResolvedValue({ success: true }),
|
|
}));
|
|
|
|
// Mock rate limiter
|
|
vi.mock("../../lib/rateLimiter", () => ({
|
|
InMemoryRateLimiter: vi.fn().mockImplementation(() => ({
|
|
checkRateLimit: vi.fn().mockReturnValue({ allowed: true }),
|
|
})),
|
|
extractClientIP: vi.fn().mockReturnValue("192.168.1.1"),
|
|
}));
|
|
|
|
describe("Authentication API Routes", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("POST /api/register", () => {
|
|
it("should register a new user successfully", async () => {
|
|
const { prisma } = await import("../../lib/prisma");
|
|
|
|
const mockCompany = {
|
|
id: "company1",
|
|
name: "Test Company",
|
|
status: "ACTIVE" as const,
|
|
csvUrl: "http://example.com/data.csv",
|
|
csvUsername: null,
|
|
csvPassword: null,
|
|
dashboardOpts: {},
|
|
maxUsers: 10,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
const mockUser = {
|
|
id: "user1",
|
|
email: "test@example.com",
|
|
name: "Test User",
|
|
companyId: "company1",
|
|
role: "USER" as const,
|
|
password: "hashed-password",
|
|
resetToken: null,
|
|
resetTokenExpiry: null,
|
|
invitedAt: null,
|
|
invitedBy: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
vi.mocked(prisma.company.findUnique).mockResolvedValue(mockCompany);
|
|
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
|
|
vi.mocked(prisma.user.create).mockResolvedValue(mockUser);
|
|
|
|
const request = new NextRequest("http://localhost:3000/api/register", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
name: "Test User",
|
|
email: "test@example.com",
|
|
password: "password123",
|
|
companyId: "company1",
|
|
}),
|
|
});
|
|
|
|
const response = await registerPOST(request);
|
|
|
|
expect(response.status).toBe(201);
|
|
const data = await response.json();
|
|
expect(data.message).toBe("User created successfully");
|
|
expect(data.user.email).toBe("test@example.com");
|
|
});
|
|
|
|
it("should return 400 for missing required fields", async () => {
|
|
const request = new NextRequest("http://localhost:3000/api/register", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
email: "test@example.com",
|
|
// Missing name, password, companyId
|
|
}),
|
|
});
|
|
|
|
const response = await registerPOST(request);
|
|
|
|
expect(response.status).toBe(400);
|
|
const data = await response.json();
|
|
expect(data.error).toBe("Missing required fields");
|
|
});
|
|
|
|
it("should return 400 for invalid email format", async () => {
|
|
const request = new NextRequest("http://localhost:3000/api/register", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
name: "Test User",
|
|
email: "invalid-email",
|
|
password: "password123",
|
|
companyId: "company1",
|
|
}),
|
|
});
|
|
|
|
const response = await registerPOST(request);
|
|
|
|
expect(response.status).toBe(400);
|
|
const data = await response.json();
|
|
expect(data.error).toBe("Invalid email format");
|
|
});
|
|
|
|
it("should return 400 for weak password", async () => {
|
|
const request = new NextRequest("http://localhost:3000/api/register", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
name: "Test User",
|
|
email: "test@example.com",
|
|
password: "123", // Too short
|
|
companyId: "company1",
|
|
}),
|
|
});
|
|
|
|
const response = await registerPOST(request);
|
|
|
|
expect(response.status).toBe(400);
|
|
const data = await response.json();
|
|
expect(data.error).toBe("Password must be at least 8 characters long");
|
|
});
|
|
|
|
it("should return 404 for non-existent company", async () => {
|
|
const { prisma } = await import("../../lib/prisma");
|
|
|
|
vi.mocked(prisma.company.findUnique).mockResolvedValue(null);
|
|
|
|
const request = new NextRequest("http://localhost:3000/api/register", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
name: "Test User",
|
|
email: "test@example.com",
|
|
password: "password123",
|
|
companyId: "non-existent",
|
|
}),
|
|
});
|
|
|
|
const response = await registerPOST(request);
|
|
|
|
expect(response.status).toBe(404);
|
|
const data = await response.json();
|
|
expect(data.error).toBe("Company not found");
|
|
});
|
|
|
|
it("should return 409 for existing user email", async () => {
|
|
const { prisma } = await import("../../lib/prisma");
|
|
|
|
const mockCompany = {
|
|
id: "company1",
|
|
name: "Test Company",
|
|
status: "ACTIVE" as const,
|
|
csvUrl: "http://example.com/data.csv",
|
|
csvUsername: null,
|
|
csvPassword: null,
|
|
dashboardOpts: {},
|
|
maxUsers: 10,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
const existingUser = {
|
|
id: "existing-user",
|
|
email: "test@example.com",
|
|
name: "Existing User",
|
|
companyId: "company1",
|
|
role: "USER" as const,
|
|
password: "hashed-password",
|
|
resetToken: null,
|
|
resetTokenExpiry: null,
|
|
invitedAt: null,
|
|
invitedBy: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
vi.mocked(prisma.company.findUnique).mockResolvedValue(mockCompany);
|
|
vi.mocked(prisma.user.findUnique).mockResolvedValue(existingUser);
|
|
|
|
const request = new NextRequest("http://localhost:3000/api/register", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
name: "Test User",
|
|
email: "test@example.com",
|
|
password: "password123",
|
|
companyId: "company1",
|
|
}),
|
|
});
|
|
|
|
const response = await registerPOST(request);
|
|
|
|
expect(response.status).toBe(409);
|
|
const data = await response.json();
|
|
expect(data.error).toBe("User already exists");
|
|
});
|
|
|
|
it("should handle rate limiting", async () => {
|
|
const { InMemoryRateLimiter } = await import("../../lib/rateLimiter");
|
|
|
|
// Mock rate limiter class constructor
|
|
const mockCheckRateLimit = vi.fn().mockReturnValue({
|
|
allowed: false,
|
|
resetTime: Date.now() + 60000,
|
|
});
|
|
|
|
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",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
name: "Test User",
|
|
email: "test@example.com",
|
|
password: "password123",
|
|
companyId: "company1",
|
|
}),
|
|
});
|
|
|
|
const response = await registerPOST(request);
|
|
|
|
expect(response.status).toBe(429);
|
|
const data = await response.json();
|
|
expect(data.error).toBe(
|
|
"Too many registration attempts. Please try again later."
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("POST /api/forgot-password", () => {
|
|
it("should send password reset email for existing user", async () => {
|
|
const { prisma } = await import("../../lib/prisma");
|
|
const { sendEmail } = await import("../../lib/sendEmail");
|
|
|
|
const existingUser = {
|
|
id: "user1",
|
|
email: "test@example.com",
|
|
name: "Test User",
|
|
companyId: "company1",
|
|
role: "USER" as const,
|
|
password: "hashed-password",
|
|
resetToken: null,
|
|
resetTokenExpiry: null,
|
|
invitedAt: null,
|
|
invitedBy: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
vi.mocked(prisma.user.findUnique).mockResolvedValue(existingUser);
|
|
vi.mocked(prisma.user.update).mockResolvedValue({
|
|
...existingUser,
|
|
resetToken: "random-token",
|
|
resetTokenExpiry: new Date(Date.now() + 3600000),
|
|
});
|
|
|
|
const request = new NextRequest(
|
|
"http://localhost:3000/api/forgot-password",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
email: "test@example.com",
|
|
}),
|
|
}
|
|
);
|
|
|
|
const response = await forgotPasswordPOST(request);
|
|
|
|
expect(response.status).toBe(200);
|
|
const data = await response.json();
|
|
expect(data.message).toBe("Password reset email sent");
|
|
expect(sendEmail).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should return success even for non-existent users (security)", async () => {
|
|
const { prisma } = await import("../../lib/prisma");
|
|
|
|
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
|
|
|
|
const request = new NextRequest(
|
|
"http://localhost:3000/api/forgot-password",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
email: "nonexistent@example.com",
|
|
}),
|
|
}
|
|
);
|
|
|
|
const response = await forgotPasswordPOST(request);
|
|
|
|
expect(response.status).toBe(200);
|
|
const data = await response.json();
|
|
expect(data.message).toBe("Password reset email sent");
|
|
});
|
|
|
|
it("should return 400 for invalid email", async () => {
|
|
const request = new NextRequest(
|
|
"http://localhost:3000/api/forgot-password",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
email: "invalid-email",
|
|
}),
|
|
}
|
|
);
|
|
|
|
const response = await forgotPasswordPOST(request);
|
|
|
|
expect(response.status).toBe(400);
|
|
const data = await response.json();
|
|
expect(data.error).toBe("Invalid email address");
|
|
});
|
|
|
|
it("should return 400 for missing email", async () => {
|
|
const request = new NextRequest(
|
|
"http://localhost:3000/api/forgot-password",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({}),
|
|
}
|
|
);
|
|
|
|
const response = await forgotPasswordPOST(request);
|
|
|
|
expect(response.status).toBe(400);
|
|
const data = await response.json();
|
|
expect(data.error).toBe("Email is required");
|
|
});
|
|
|
|
it("should handle database errors gracefully", async () => {
|
|
const { prisma } = await import("../../lib/prisma");
|
|
|
|
vi.mocked(prisma.user.findUnique).mockRejectedValue(
|
|
new Error("Database connection failed")
|
|
);
|
|
|
|
const request = new NextRequest(
|
|
"http://localhost:3000/api/forgot-password",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
email: "test@example.com",
|
|
}),
|
|
}
|
|
);
|
|
|
|
const response = await forgotPasswordPOST(request);
|
|
|
|
expect(response.status).toBe(500);
|
|
const data = await response.json();
|
|
expect(data.error).toBe("Internal server error");
|
|
});
|
|
|
|
it("should handle email sending failures gracefully", async () => {
|
|
const { prisma } = await import("../../lib/prisma");
|
|
const { sendEmail } = await import("../../lib/sendEmail");
|
|
|
|
const existingUser = {
|
|
id: "user1",
|
|
email: "test@example.com",
|
|
name: "Test User",
|
|
companyId: "company1",
|
|
role: "USER" as const,
|
|
password: "hashed-password",
|
|
resetToken: null,
|
|
resetTokenExpiry: null,
|
|
invitedAt: null,
|
|
invitedBy: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
vi.mocked(prisma.user.findUnique).mockResolvedValue(existingUser);
|
|
vi.mocked(prisma.user.update).mockResolvedValue({
|
|
...existingUser,
|
|
resetToken: "random-token",
|
|
resetTokenExpiry: new Date(Date.now() + 3600000),
|
|
});
|
|
vi.mocked(sendEmail).mockResolvedValue({
|
|
success: false,
|
|
error: "Email service unavailable",
|
|
});
|
|
|
|
const request = new NextRequest(
|
|
"http://localhost:3000/api/forgot-password",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
email: "test@example.com",
|
|
}),
|
|
}
|
|
);
|
|
|
|
const response = await forgotPasswordPOST(request);
|
|
|
|
expect(response.status).toBe(200);
|
|
const data = await response.json();
|
|
expect(data.message).toBe("Password reset email sent");
|
|
});
|
|
});
|
|
});
|