mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 14:12:10 +01:00
feat: implement comprehensive email system with rate limiting and extensive test suite
- Add robust email service with rate limiting and configuration management - Implement shared rate limiter utility for consistent API protection - Create comprehensive test suite for core processing pipeline - Add API tests for dashboard metrics and authentication routes - Fix date range picker infinite loop issue - Improve session lookup in refresh sessions API - Refactor session API routing with better code organization - Update processing pipeline status monitoring - Clean up leftover files and improve code formatting
This commit is contained in:
458
tests/api/auth-routes.test.ts
Normal file
458
tests/api/auth-routes.test.ts
Normal file
@ -0,0 +1,458 @@
|
||||
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",
|
||||
csvUrl: "http://example.com/data.csv",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const mockUser = {
|
||||
id: "user1",
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
companyId: "company1",
|
||||
role: "USER",
|
||||
password: "hashed-password",
|
||||
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",
|
||||
csvUrl: "http://example.com/data.csv",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const existingUser = {
|
||||
id: "existing-user",
|
||||
email: "test@example.com",
|
||||
name: "Existing User",
|
||||
companyId: "company1",
|
||||
role: "USER",
|
||||
password: "hashed-password",
|
||||
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 to return not allowed
|
||||
const mockRateLimiter = {
|
||||
checkRateLimit: vi.fn().mockReturnValue({
|
||||
allowed: false,
|
||||
resetTime: Date.now() + 60000,
|
||||
}),
|
||||
};
|
||||
|
||||
vi.mocked(InMemoryRateLimiter).mockImplementation(() => mockRateLimiter);
|
||||
|
||||
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",
|
||||
password: "hashed-password",
|
||||
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",
|
||||
password: "hashed-password",
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user