This commit is contained in:
2026-01-23 20:35:40 +03:00
parent cf3b0e48ec
commit 66c151653e
137 changed files with 41495 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
import { describe, it, expect } from "vitest";
import bcrypt from "bcryptjs";
// Test the actual bcrypt functionality without importing our modules
describe("Authentication Integration", () => {
describe("Password Hashing Integration", () => {
it("should hash and verify passwords using bcrypt", async () => {
const password = "testpassword123";
// Hash the password
const hashedPassword = await bcrypt.hash(password, 12);
expect(hashedPassword).toBeDefined();
expect(hashedPassword).not.toBe(password);
expect(hashedPassword.length).toBeGreaterThan(0);
// Verify correct password
const isValidPassword = await bcrypt.compare(password, hashedPassword);
expect(isValidPassword).toBe(true);
// Verify incorrect password
const isInvalidPassword = await bcrypt.compare("wrongpassword", hashedPassword);
expect(isInvalidPassword).toBe(false);
});
it("should generate different hashes for the same password", async () => {
const password = "testpassword123";
const hash1 = await bcrypt.hash(password, 12);
const hash2 = await bcrypt.hash(password, 12);
expect(hash1).not.toBe(hash2);
// But both should verify correctly
expect(await bcrypt.compare(password, hash1)).toBe(true);
expect(await bcrypt.compare(password, hash2)).toBe(true);
});
});
});

View File

@@ -0,0 +1,58 @@
import { describe, it, expect, vi } from "vitest";
import { hasPermission, canAccessUserManagement } from "../auth-helpers.server";
import { AUTH_LEVELS } from "~/types/auth";
// Mock the database
vi.mock("../db.server", () => ({
prisma: {
user: {
findFirst: vi.fn(),
findUnique: vi.fn(),
create: vi.fn(),
count: vi.fn(),
},
},
}));
// Mock auth.server to avoid session secret requirement
vi.mock("../auth.server", () => ({
hashPassword: vi.fn(),
verifyPassword: vi.fn(),
createUserSession: vi.fn(),
getUserSession: vi.fn(),
getUserId: vi.fn(),
requireUserId: vi.fn(),
getUser: vi.fn(),
requireUser: vi.fn(),
logout: vi.fn(),
}));
describe("Authentication System", () => {
describe("Authorization Helpers", () => {
it("should check permissions correctly", () => {
// Superadmin should have access to everything
expect(hasPermission(AUTH_LEVELS.SUPERADMIN, AUTH_LEVELS.SUPERADMIN)).toBe(true);
expect(hasPermission(AUTH_LEVELS.SUPERADMIN, AUTH_LEVELS.ADMIN)).toBe(true);
expect(hasPermission(AUTH_LEVELS.SUPERADMIN, AUTH_LEVELS.USER)).toBe(true);
// Admin should have access to admin and user levels
expect(hasPermission(AUTH_LEVELS.ADMIN, AUTH_LEVELS.SUPERADMIN)).toBe(false);
expect(hasPermission(AUTH_LEVELS.ADMIN, AUTH_LEVELS.ADMIN)).toBe(true);
expect(hasPermission(AUTH_LEVELS.ADMIN, AUTH_LEVELS.USER)).toBe(true);
// User should only have access to user level
expect(hasPermission(AUTH_LEVELS.USER, AUTH_LEVELS.SUPERADMIN)).toBe(false);
expect(hasPermission(AUTH_LEVELS.USER, AUTH_LEVELS.ADMIN)).toBe(false);
expect(hasPermission(AUTH_LEVELS.USER, AUTH_LEVELS.USER)).toBe(true);
});
it("should check user management access correctly", () => {
expect(canAccessUserManagement(AUTH_LEVELS.SUPERADMIN)).toBe(true);
expect(canAccessUserManagement(AUTH_LEVELS.ADMIN)).toBe(true);
expect(canAccessUserManagement(AUTH_LEVELS.USER)).toBe(false);
});
});
});

View File

@@ -0,0 +1,437 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
// Mock Prisma first
vi.mock('../db.server', () => ({
prisma: {
customer: {
findMany: vi.fn(),
findUnique: vi.fn(),
findFirst: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
count: vi.fn(),
},
},
}));
import {
getCustomers,
createCustomer,
updateCustomer,
deleteCustomer,
getCustomerById,
getCustomersForSelect,
searchCustomers,
getCustomerStats
} from '../customer-management.server';
import { prisma } from '../db.server';
const mockPrisma = prisma as any;
describe('Customer Management Server', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('getCustomers', () => {
it('should return customers with pagination', async () => {
const mockCustomers = [
{
id: 1,
name: 'أحمد محمد',
phone: '0501234567',
email: 'ahmed@example.com',
address: 'الرياض',
createdDate: new Date(),
updateDate: new Date(),
vehicles: [],
maintenanceVisits: [],
},
];
mockPrisma.customer.findMany.mockResolvedValue(mockCustomers);
mockPrisma.customer.count.mockResolvedValue(1);
const result = await getCustomers('', 1, 10);
expect(result).toEqual({
customers: mockCustomers,
total: 1,
totalPages: 1,
});
expect(mockPrisma.customer.findMany).toHaveBeenCalledWith({
where: {},
include: {
vehicles: {
select: {
id: true,
plateNumber: true,
manufacturer: true,
model: true,
year: true,
lastVisitDate: true,
suggestedNextVisitDate: true,
},
},
maintenanceVisits: {
select: {
id: true,
visitDate: true,
cost: true,
maintenanceType: true,
},
orderBy: { visitDate: 'desc' },
take: 5,
},
},
orderBy: { createdDate: 'desc' },
skip: 0,
take: 10,
});
});
it('should handle search query', async () => {
mockPrisma.customer.findMany.mockResolvedValue([]);
mockPrisma.customer.count.mockResolvedValue(0);
await getCustomers('أحمد', 1, 10);
expect(mockPrisma.customer.findMany).toHaveBeenCalledWith({
where: {
OR: [
{ name: { contains: 'أحمد' } },
{ phone: { contains: 'أحمد' } },
{ email: { contains: 'أحمد' } },
{ address: { contains: 'أحمد' } },
],
},
include: expect.any(Object),
orderBy: { createdDate: 'desc' },
skip: 0,
take: 10,
});
});
});
describe('createCustomer', () => {
it('should create a new customer successfully', async () => {
const customerData = {
name: 'أحمد محمد',
phone: '0501234567',
email: 'ahmed@example.com',
address: 'الرياض',
};
const mockCustomer = {
id: 1,
...customerData,
createdDate: new Date(),
updateDate: new Date(),
};
mockPrisma.customer.findFirst.mockResolvedValue(null);
mockPrisma.customer.create.mockResolvedValue(mockCustomer);
const result = await createCustomer(customerData);
expect(result).toEqual({
success: true,
customer: mockCustomer,
});
expect(mockPrisma.customer.create).toHaveBeenCalledWith({
data: {
name: 'أحمد محمد',
phone: '0501234567',
email: 'ahmed@example.com',
address: 'الرياض',
},
});
});
it('should return error if phone already exists', async () => {
const customerData = {
name: 'أحمد محمد',
phone: '0501234567',
};
const existingCustomer = {
id: 2,
name: 'محمد أحمد',
phone: '0501234567',
email: null,
address: null,
createdDate: new Date(),
updateDate: new Date(),
};
mockPrisma.customer.findFirst.mockResolvedValue(existingCustomer);
const result = await createCustomer(customerData);
expect(result).toEqual({
success: false,
error: 'رقم الهاتف موجود بالفعل',
});
expect(mockPrisma.customer.create).not.toHaveBeenCalled();
});
it('should return error if email already exists', async () => {
const customerData = {
name: 'أحمد محمد',
email: 'ahmed@example.com',
};
const existingCustomer = {
id: 2,
name: 'محمد أحمد',
phone: null,
email: 'ahmed@example.com',
address: null,
createdDate: new Date(),
updateDate: new Date(),
};
mockPrisma.customer.findFirst.mockResolvedValue(existingCustomer);
const result = await createCustomer(customerData);
expect(result).toEqual({
success: false,
error: 'البريد الإلكتروني موجود بالفعل',
});
expect(mockPrisma.customer.create).not.toHaveBeenCalled();
});
});
describe('updateCustomer', () => {
it('should update customer successfully', async () => {
const customerId = 1;
const updateData = {
name: 'أحمد محمد المحدث',
phone: '0509876543',
};
const existingCustomer = {
id: customerId,
name: 'أحمد محمد',
phone: '0501234567',
email: null,
address: null,
createdDate: new Date(),
updateDate: new Date(),
};
const updatedCustomer = {
...existingCustomer,
...updateData,
updateDate: new Date(),
};
mockPrisma.customer.findUnique.mockResolvedValue(existingCustomer);
mockPrisma.customer.findFirst.mockResolvedValue(null);
mockPrisma.customer.update.mockResolvedValue(updatedCustomer);
const result = await updateCustomer(customerId, updateData);
expect(result).toEqual({
success: true,
customer: updatedCustomer,
});
expect(mockPrisma.customer.update).toHaveBeenCalledWith({
where: { id: customerId },
data: {
name: 'أحمد محمد المحدث',
phone: '0509876543',
},
});
});
it('should return error if customer not found', async () => {
const customerId = 999;
const updateData = { name: 'أحمد محمد' };
mockPrisma.customer.findUnique.mockResolvedValue(null);
const result = await updateCustomer(customerId, updateData);
expect(result).toEqual({
success: false,
error: 'العميل غير موجود',
});
expect(mockPrisma.customer.update).not.toHaveBeenCalled();
});
});
describe('deleteCustomer', () => {
it('should delete customer successfully when no relationships exist', async () => {
const customerId = 1;
const existingCustomer = {
id: customerId,
name: 'أحمد محمد',
phone: '0501234567',
email: null,
address: null,
createdDate: new Date(),
updateDate: new Date(),
vehicles: [],
maintenanceVisits: [],
};
mockPrisma.customer.findUnique.mockResolvedValue(existingCustomer);
mockPrisma.customer.delete.mockResolvedValue(existingCustomer);
const result = await deleteCustomer(customerId);
expect(result).toEqual({
success: true,
});
expect(mockPrisma.customer.delete).toHaveBeenCalledWith({
where: { id: customerId },
});
});
it('should return error if customer has vehicles', async () => {
const customerId = 1;
const existingCustomer = {
id: customerId,
name: 'أحمد محمد',
phone: '0501234567',
email: null,
address: null,
createdDate: new Date(),
updateDate: new Date(),
vehicles: [{ id: 1, plateNumber: 'ABC-123' }],
maintenanceVisits: [],
};
mockPrisma.customer.findUnique.mockResolvedValue(existingCustomer);
const result = await deleteCustomer(customerId);
expect(result).toEqual({
success: false,
error: 'لا يمكن حذف العميل لأنه يملك 1 مركبة. يرجى حذف المركبات أولاً',
});
expect(mockPrisma.customer.delete).not.toHaveBeenCalled();
});
it('should return error if customer has maintenance visits', async () => {
const customerId = 1;
const existingCustomer = {
id: customerId,
name: 'أحمد محمد',
phone: '0501234567',
email: null,
address: null,
createdDate: new Date(),
updateDate: new Date(),
vehicles: [],
maintenanceVisits: [{ id: 1, visitDate: new Date() }],
};
mockPrisma.customer.findUnique.mockResolvedValue(existingCustomer);
const result = await deleteCustomer(customerId);
expect(result).toEqual({
success: false,
error: 'لا يمكن حذف العميل لأنه لديه 1 زيارة صيانة. يرجى حذف الزيارات أولاً',
});
expect(mockPrisma.customer.delete).not.toHaveBeenCalled();
});
it('should return error if customer not found', async () => {
const customerId = 999;
mockPrisma.customer.findUnique.mockResolvedValue(null);
const result = await deleteCustomer(customerId);
expect(result).toEqual({
success: false,
error: 'العميل غير موجود',
});
expect(mockPrisma.customer.delete).not.toHaveBeenCalled();
});
});
describe('searchCustomers', () => {
it('should return empty array for short queries', async () => {
const result = await searchCustomers('a');
expect(result).toEqual([]);
expect(mockPrisma.customer.findMany).not.toHaveBeenCalled();
});
it('should search customers by name, phone, and email', async () => {
const mockCustomers = [
{
id: 1,
name: 'أحمد محمد',
phone: '0501234567',
email: 'ahmed@example.com',
},
];
mockPrisma.customer.findMany.mockResolvedValue(mockCustomers);
const result = await searchCustomers('أحمد', 10);
expect(result).toEqual(mockCustomers);
expect(mockPrisma.customer.findMany).toHaveBeenCalledWith({
where: {
OR: [
{ name: { contains: 'أحمد' } },
{ phone: { contains: 'أحمد' } },
{ email: { contains: 'أحمد' } },
],
},
select: {
id: true,
name: true,
phone: true,
email: true,
},
orderBy: { name: 'asc' },
take: 10,
});
});
});
describe('getCustomerStats', () => {
it('should return customer statistics', async () => {
const customerId = 1;
const mockCustomer = {
id: customerId,
name: 'أحمد محمد',
vehicles: [{ id: 1 }, { id: 2 }],
maintenanceVisits: [
{ cost: 100, visitDate: new Date('2024-01-15') },
{ cost: 200, visitDate: new Date('2024-01-10') },
{ cost: 150, visitDate: new Date('2024-01-05') },
],
};
mockPrisma.customer.findUnique.mockResolvedValue(mockCustomer);
const result = await getCustomerStats(customerId);
expect(result).toEqual({
totalVehicles: 2,
totalVisits: 3,
totalSpent: 450,
lastVisitDate: new Date('2024-01-15'),
});
});
it('should return null if customer not found', async () => {
const customerId = 999;
mockPrisma.customer.findUnique.mockResolvedValue(null);
const result = await getCustomerStats(customerId);
expect(result).toBeNull();
});
});
});

View File

@@ -0,0 +1,293 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock the auth middleware
vi.mock('../auth-middleware.server', () => ({
requireAuth: vi.fn().mockResolvedValue({
id: 1,
name: 'Test User',
username: 'testuser',
email: 'test@example.com',
authLevel: 1,
status: 'active',
}),
}));
// Mock the customer management functions
vi.mock('../customer-management.server', () => ({
getCustomers: vi.fn(),
createCustomer: vi.fn(),
updateCustomer: vi.fn(),
deleteCustomer: vi.fn(),
getCustomerById: vi.fn(),
}));
// Mock validation
vi.mock('../validation', () => ({
validateCustomer: vi.fn(),
}));
import { loader, action } from '../../routes/customers';
import { getCustomers, createCustomer, updateCustomer, deleteCustomer } from '../customer-management.server';
import { validateCustomer } from '../validation';
const mockGetCustomers = getCustomers as any;
const mockCreateCustomer = createCustomer as any;
const mockUpdateCustomer = updateCustomer as any;
const mockDeleteCustomer = deleteCustomer as any;
const mockValidateCustomer = validateCustomer as any;
describe('Customer Routes Integration', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('loader', () => {
it('should load customers with default pagination', async () => {
const mockCustomers = [
{
id: 1,
name: 'أحمد محمد',
phone: '0501234567',
email: 'ahmed@example.com',
address: 'الرياض',
createdDate: new Date(),
updateDate: new Date(),
vehicles: [],
maintenanceVisits: [],
},
];
mockGetCustomers.mockResolvedValue({
customers: mockCustomers,
total: 1,
totalPages: 1,
});
const request = new Request('http://localhost:3000/customers');
const response = await loader({ request, params: {}, context: {} });
const data = await response.json();
expect(mockGetCustomers).toHaveBeenCalledWith('', 1, 10);
expect(data.customers).toEqual(mockCustomers);
expect(data.total).toBe(1);
expect(data.totalPages).toBe(1);
expect(data.currentPage).toBe(1);
});
it('should handle search and pagination parameters', async () => {
mockGetCustomers.mockResolvedValue({
customers: [],
total: 0,
totalPages: 0,
});
const request = new Request('http://localhost:3000/customers?search=أحمد&page=2&limit=20');
await loader({ request, params: {}, context: {} });
expect(mockGetCustomers).toHaveBeenCalledWith('أحمد', 2, 20);
});
});
describe('action', () => {
it('should create customer successfully', async () => {
const mockCustomer = {
id: 1,
name: 'أحمد محمد',
phone: '0501234567',
email: 'ahmed@example.com',
address: 'الرياض',
createdDate: new Date(),
updateDate: new Date(),
};
mockValidateCustomer.mockReturnValue({
isValid: true,
errors: {},
});
mockCreateCustomer.mockResolvedValue({
success: true,
customer: mockCustomer,
});
const formData = new FormData();
formData.append('_action', 'create');
formData.append('name', 'أحمد محمد');
formData.append('phone', '0501234567');
formData.append('email', 'ahmed@example.com');
formData.append('address', 'الرياض');
const request = new Request('http://localhost:3000/customers', {
method: 'POST',
body: formData,
});
const response = await action({ request, params: {}, context: {} });
const data = await response.json();
expect(mockValidateCustomer).toHaveBeenCalledWith({
name: 'أحمد محمد',
phone: '0501234567',
email: 'ahmed@example.com',
address: 'الرياض',
});
expect(mockCreateCustomer).toHaveBeenCalledWith({
name: 'أحمد محمد',
phone: '0501234567',
email: 'ahmed@example.com',
address: 'الرياض',
});
expect(data.success).toBe(true);
expect(data.customer).toEqual(mockCustomer);
expect(data.action).toBe('create');
expect(data.message).toBe('تم إنشاء العميل بنجاح');
});
it('should return validation errors for invalid data', async () => {
mockValidateCustomer.mockReturnValue({
isValid: false,
errors: {
name: 'اسم العميل مطلوب',
email: 'البريد الإلكتروني غير صحيح',
},
});
const formData = new FormData();
formData.append('_action', 'create');
formData.append('name', '');
formData.append('email', 'invalid-email');
const request = new Request('http://localhost:3000/customers', {
method: 'POST',
body: formData,
});
const response = await action({ request, params: {}, context: {} });
const data = await response.json();
expect(response.status).toBe(400);
expect(data.success).toBe(false);
expect(data.errors).toEqual({
name: 'اسم العميل مطلوب',
email: 'البريد الإلكتروني غير صحيح',
});
expect(mockCreateCustomer).not.toHaveBeenCalled();
});
it('should update customer successfully', async () => {
const mockCustomer = {
id: 1,
name: 'أحمد محمد المحدث',
phone: '0509876543',
email: 'ahmed.updated@example.com',
address: 'جدة',
createdDate: new Date(),
updateDate: new Date(),
};
mockValidateCustomer.mockReturnValue({
isValid: true,
errors: {},
});
mockUpdateCustomer.mockResolvedValue({
success: true,
customer: mockCustomer,
});
const formData = new FormData();
formData.append('_action', 'update');
formData.append('id', '1');
formData.append('name', 'أحمد محمد المحدث');
formData.append('phone', '0509876543');
formData.append('email', 'ahmed.updated@example.com');
formData.append('address', 'جدة');
const request = new Request('http://localhost:3000/customers', {
method: 'POST',
body: formData,
});
const response = await action({ request, params: {}, context: {} });
const data = await response.json();
expect(mockUpdateCustomer).toHaveBeenCalledWith(1, {
name: 'أحمد محمد المحدث',
phone: '0509876543',
email: 'ahmed.updated@example.com',
address: 'جدة',
});
expect(data.success).toBe(true);
expect(data.customer).toEqual(mockCustomer);
expect(data.action).toBe('update');
expect(data.message).toBe('تم تحديث العميل بنجاح');
});
it('should delete customer successfully', async () => {
mockDeleteCustomer.mockResolvedValue({
success: true,
});
const formData = new FormData();
formData.append('_action', 'delete');
formData.append('id', '1');
const request = new Request('http://localhost:3000/customers', {
method: 'POST',
body: formData,
});
const response = await action({ request, params: {}, context: {} });
const data = await response.json();
expect(mockDeleteCustomer).toHaveBeenCalledWith(1);
expect(data.success).toBe(true);
expect(data.action).toBe('delete');
expect(data.message).toBe('تم حذف العميل بنجاح');
});
it('should handle delete errors', async () => {
mockDeleteCustomer.mockResolvedValue({
success: false,
error: 'لا يمكن حذف العميل لأنه يملك مركبات',
});
const formData = new FormData();
formData.append('_action', 'delete');
formData.append('id', '1');
const request = new Request('http://localhost:3000/customers', {
method: 'POST',
body: formData,
});
const response = await action({ request, params: {}, context: {} });
const data = await response.json();
expect(response.status).toBe(400);
expect(data.success).toBe(false);
expect(data.error).toBe('لا يمكن حذف العميل لأنه يملك مركبات');
});
it('should handle unknown action', async () => {
const formData = new FormData();
formData.append('_action', 'unknown');
const request = new Request('http://localhost:3000/customers', {
method: 'POST',
body: formData,
});
const response = await action({ request, params: {}, context: {} });
const data = await response.json();
expect(response.status).toBe(400);
expect(data.success).toBe(false);
expect(data.error).toBe('إجراء غير صحيح');
expect(data.action).toBe('unknown');
});
});
});

View File

@@ -0,0 +1,109 @@
import { describe, it, expect } from 'vitest';
import { validateCustomer } from '../validation';
describe('Customer Validation', () => {
describe('validateCustomer', () => {
it('should validate required name field', () => {
const result = validateCustomer({ name: '' });
expect(result.isValid).toBe(false);
expect(result.errors.name).toBe('اسم العميل مطلوب');
});
it('should validate name length', () => {
const longName = 'أ'.repeat(101);
const result = validateCustomer({ name: longName });
expect(result.isValid).toBe(false);
expect(result.errors.name).toBe('الاسم يجب أن يكون أقل من 100 حرف');
});
it('should validate phone format', () => {
const result = validateCustomer({
name: 'أحمد محمد',
phone: 'invalid-phone'
});
expect(result.isValid).toBe(false);
expect(result.errors.phone).toBe('رقم الهاتف غير صحيح');
});
it('should accept valid phone formats', () => {
const validPhones = [
'0501234567',
'+966501234567',
'050 123 4567',
'050-123-4567',
'(050) 123-4567',
];
validPhones.forEach(phone => {
const result = validateCustomer({
name: 'أحمد محمد',
phone
});
expect(result.isValid).toBe(true);
expect(result.errors.phone).toBeUndefined();
});
});
it('should validate email format', () => {
const result = validateCustomer({
name: 'أحمد محمد',
email: 'invalid-email'
});
expect(result.isValid).toBe(false);
expect(result.errors.email).toBe('البريد الإلكتروني غير صحيح');
});
it('should accept valid email format', () => {
const result = validateCustomer({
name: 'أحمد محمد',
email: 'ahmed@example.com'
});
expect(result.isValid).toBe(true);
expect(result.errors.email).toBeUndefined();
});
it('should allow empty optional fields', () => {
const result = validateCustomer({
name: 'أحمد محمد',
phone: '',
email: '',
address: ''
});
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
it('should validate complete customer data', () => {
const result = validateCustomer({
name: 'أحمد محمد',
phone: '0501234567',
email: 'ahmed@example.com',
address: 'الرياض، المملكة العربية السعودية'
});
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
it('should handle undefined fields gracefully', () => {
const result = validateCustomer({
name: 'أحمد محمد',
phone: undefined,
email: undefined,
address: undefined
});
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
it('should trim whitespace from name', () => {
const result = validateCustomer({ name: ' أحمد محمد ' });
expect(result.isValid).toBe(true);
});
it('should reject name with only whitespace', () => {
const result = validateCustomer({ name: ' ' });
expect(result.isValid).toBe(false);
expect(result.errors.name).toBe('اسم العميل مطلوب');
});
});
});

View File

@@ -0,0 +1,278 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
createExpense,
getExpenses,
getExpenseById,
updateExpense,
deleteExpense,
getExpenseCategories,
getExpensesByCategory,
getTotalExpenses
} from '../expense-management.server';
import { prisma } from '../db.server';
describe('Expense Management', () => {
beforeEach(async () => {
// Clean up test data
await prisma.expense.deleteMany();
});
afterEach(async () => {
// Clean up test data
await prisma.expense.deleteMany();
});
describe('createExpense', () => {
it('should create a new expense', async () => {
const expenseData = {
description: 'قطع غيار للمحرك',
category: 'قطع غيار',
amount: 500.00,
expenseDate: new Date('2024-01-15'),
};
const expense = await createExpense(expenseData);
expect(expense).toBeDefined();
expect(expense.description).toBe(expenseData.description);
expect(expense.category).toBe(expenseData.category);
expect(expense.amount).toBe(expenseData.amount);
expect(expense.expenseDate).toEqual(expenseData.expenseDate);
});
it('should create expense with current date if no date provided', async () => {
const expenseData = {
description: 'مصروف عام',
category: 'أخرى',
amount: 100.00,
};
const expense = await createExpense(expenseData);
expect(expense).toBeDefined();
expect(expense.expenseDate).toBeInstanceOf(Date);
});
});
describe('getExpenses', () => {
beforeEach(async () => {
// Create test expenses
await prisma.expense.createMany({
data: [
{
description: 'قطع غيار',
category: 'قطع غيار',
amount: 500.00,
expenseDate: new Date('2024-01-15'),
},
{
description: 'أدوات صيانة',
category: 'أدوات',
amount: 200.00,
expenseDate: new Date('2024-01-10'),
},
{
description: 'إيجار المحل',
category: 'إيجار',
amount: 3000.00,
expenseDate: new Date('2024-01-01'),
},
],
});
});
it('should get all expenses with pagination', async () => {
const result = await getExpenses();
expect(result.expenses).toHaveLength(3);
expect(result.total).toBe(3);
expect(result.totalPages).toBe(1);
});
it('should filter expenses by search query', async () => {
const result = await getExpenses('قطع غيار');
expect(result.expenses).toHaveLength(1);
expect(result.expenses[0].description).toBe('قطع غيار');
});
it('should filter expenses by category', async () => {
const result = await getExpenses(undefined, 1, 10, 'أدوات');
expect(result.expenses).toHaveLength(1);
expect(result.expenses[0].category).toBe('أدوات');
});
it('should filter expenses by date range', async () => {
const dateFrom = new Date('2024-01-10');
const dateTo = new Date('2024-01-20');
const result = await getExpenses(undefined, 1, 10, undefined, dateFrom, dateTo);
expect(result.expenses).toHaveLength(2);
});
it('should handle pagination correctly', async () => {
const result = await getExpenses(undefined, 1, 2);
expect(result.expenses).toHaveLength(2);
expect(result.totalPages).toBe(2);
});
});
describe('getExpenseById', () => {
it('should get expense by ID', async () => {
const createdExpense = await prisma.expense.create({
data: {
description: 'تست مصروف',
category: 'أخرى',
amount: 100.00,
},
});
const expense = await getExpenseById(createdExpense.id);
expect(expense).toBeDefined();
expect(expense?.id).toBe(createdExpense.id);
expect(expense?.description).toBe('تست مصروف');
});
it('should return null for non-existent expense', async () => {
const expense = await getExpenseById(999);
expect(expense).toBeNull();
});
});
describe('updateExpense', () => {
it('should update expense successfully', async () => {
const createdExpense = await prisma.expense.create({
data: {
description: 'مصروف قديم',
category: 'أخرى',
amount: 100.00,
},
});
const updateData = {
description: 'مصروف محدث',
category: 'قطع غيار',
amount: 200.00,
};
const updatedExpense = await updateExpense(createdExpense.id, updateData);
expect(updatedExpense.description).toBe(updateData.description);
expect(updatedExpense.category).toBe(updateData.category);
expect(updatedExpense.amount).toBe(updateData.amount);
});
});
describe('deleteExpense', () => {
it('should delete expense successfully', async () => {
const createdExpense = await prisma.expense.create({
data: {
description: 'مصروف للحذف',
category: 'أخرى',
amount: 100.00,
},
});
await deleteExpense(createdExpense.id);
const deletedExpense = await prisma.expense.findUnique({
where: { id: createdExpense.id },
});
expect(deletedExpense).toBeNull();
});
});
describe('getExpenseCategories', () => {
beforeEach(async () => {
await prisma.expense.createMany({
data: [
{ description: 'مصروف 1', category: 'قطع غيار', amount: 100 },
{ description: 'مصروف 2', category: 'أدوات', amount: 200 },
{ description: 'مصروف 3', category: 'قطع غيار', amount: 150 },
],
});
});
it('should get unique expense categories', async () => {
const categories = await getExpenseCategories();
expect(categories).toHaveLength(2);
expect(categories).toContain('قطع غيار');
expect(categories).toContain('أدوات');
});
});
describe('getExpensesByCategory', () => {
beforeEach(async () => {
await prisma.expense.createMany({
data: [
{ description: 'مصروف 1', category: 'قطع غيار', amount: 100 },
{ description: 'مصروف 2', category: 'أدوات', amount: 200 },
{ description: 'مصروف 3', category: 'قطع غيار', amount: 150 },
],
});
});
it('should group expenses by category', async () => {
const result = await getExpensesByCategory();
expect(result).toHaveLength(2);
const spareParts = result.find(r => r.category === 'قطع غيار');
expect(spareParts?.total).toBe(250);
expect(spareParts?.count).toBe(2);
const tools = result.find(r => r.category === 'أدوات');
expect(tools?.total).toBe(200);
expect(tools?.count).toBe(1);
});
});
describe('getTotalExpenses', () => {
beforeEach(async () => {
await prisma.expense.createMany({
data: [
{
description: 'مصروف 1',
category: 'قطع غيار',
amount: 100,
expenseDate: new Date('2024-01-15'),
},
{
description: 'مصروف 2',
category: 'أدوات',
amount: 200,
expenseDate: new Date('2024-01-10'),
},
{
description: 'مصروف 3',
category: 'إيجار',
amount: 3000,
expenseDate: new Date('2023-12-15'),
},
],
});
});
it('should calculate total expenses', async () => {
const total = await getTotalExpenses();
expect(total).toBe(3300);
});
it('should calculate total expenses for date range', async () => {
const dateFrom = new Date('2024-01-01');
const dateTo = new Date('2024-01-31');
const total = await getTotalExpenses(dateFrom, dateTo);
expect(total).toBe(300); // Only expenses from January 2024
});
});
});

View File

@@ -0,0 +1,458 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
getFinancialSummary,
getMonthlyFinancialData,
getIncomeByMaintenanceType,
getExpenseBreakdown,
getTopCustomersByRevenue,
getFinancialTrends
} from '../financial-reporting.server';
import { prisma } from '../db.server';
describe('Financial Reporting', () => {
beforeEach(async () => {
// Clean up test data
await prisma.income.deleteMany();
await prisma.expense.deleteMany();
await prisma.maintenanceVisit.deleteMany();
await prisma.vehicle.deleteMany();
await prisma.customer.deleteMany();
});
afterEach(async () => {
// Clean up test data
await prisma.income.deleteMany();
await prisma.expense.deleteMany();
await prisma.maintenanceVisit.deleteMany();
await prisma.vehicle.deleteMany();
await prisma.customer.deleteMany();
});
describe('getFinancialSummary', () => {
beforeEach(async () => {
// Create test customer
const customer = await prisma.customer.create({
data: {
name: 'عميل تجريبي',
phone: '0501234567',
},
});
// Create test vehicle
const vehicle = await prisma.vehicle.create({
data: {
plateNumber: 'ABC-123',
bodyType: 'سيدان',
manufacturer: 'تويوتا',
model: 'كامري',
year: 2020,
transmission: 'Automatic',
fuel: 'Gasoline',
useType: 'personal',
ownerId: customer.id,
},
});
// Create test maintenance visit
const visit = await prisma.maintenanceVisit.create({
data: {
vehicleId: vehicle.id,
customerId: customer.id,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 200.00,
kilometers: 50000,
nextVisitDelay: 3,
},
});
// Create test income
await prisma.income.create({
data: {
maintenanceVisitId: visit.id,
amount: 200.00,
incomeDate: new Date('2024-01-15'),
},
});
// Create test expenses
await prisma.expense.createMany({
data: [
{
description: 'قطع غيار',
category: 'قطع غيار',
amount: 50.00,
expenseDate: new Date('2024-01-10'),
},
{
description: 'أدوات',
category: 'أدوات',
amount: 30.00,
expenseDate: new Date('2024-01-12'),
},
],
});
});
it('should calculate financial summary correctly', async () => {
const summary = await getFinancialSummary();
expect(summary.totalIncome).toBe(200.00);
expect(summary.totalExpenses).toBe(80.00);
expect(summary.netProfit).toBe(120.00);
expect(summary.incomeCount).toBe(1);
expect(summary.expenseCount).toBe(2);
expect(summary.profitMargin).toBe(60.0); // (120/200) * 100
});
it('should filter by date range', async () => {
const dateFrom = new Date('2024-01-11');
const dateTo = new Date('2024-01-20');
const summary = await getFinancialSummary(dateFrom, dateTo);
expect(summary.totalIncome).toBe(200.00);
expect(summary.totalExpenses).toBe(30.00); // Only one expense in range
expect(summary.netProfit).toBe(170.00);
});
});
describe('getIncomeByMaintenanceType', () => {
beforeEach(async () => {
// Create test data
const customer = await prisma.customer.create({
data: { name: 'عميل تجريبي', phone: '0501234567' },
});
const vehicle = await prisma.vehicle.create({
data: {
plateNumber: 'ABC-123',
bodyType: 'سيدان',
manufacturer: 'تويوتا',
model: 'كامري',
year: 2020,
transmission: 'Automatic',
fuel: 'Gasoline',
useType: 'personal',
ownerId: customer.id,
},
});
// Create maintenance visits with different types
const visit1 = await prisma.maintenanceVisit.create({
data: {
vehicleId: vehicle.id,
customerId: customer.id,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 200.00,
kilometers: 50000,
nextVisitDelay: 3,
},
});
const visit2 = await prisma.maintenanceVisit.create({
data: {
vehicleId: vehicle.id,
customerId: customer.id,
maintenanceType: 'فحص دوري',
description: 'فحص دوري شامل',
cost: 150.00,
kilometers: 51000,
nextVisitDelay: 3,
},
});
const visit3 = await prisma.maintenanceVisit.create({
data: {
vehicleId: vehicle.id,
customerId: customer.id,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت مرة أخرى',
cost: 180.00,
kilometers: 52000,
nextVisitDelay: 3,
},
});
// Create corresponding income records
await prisma.income.createMany({
data: [
{ maintenanceVisitId: visit1.id, amount: 200.00 },
{ maintenanceVisitId: visit2.id, amount: 150.00 },
{ maintenanceVisitId: visit3.id, amount: 180.00 },
],
});
});
it('should group income by maintenance type', async () => {
const result = await getIncomeByMaintenanceType();
expect(result).toHaveLength(2);
const oilChange = result.find(r => r.category === 'تغيير زيت');
expect(oilChange?.amount).toBe(380.00);
expect(oilChange?.count).toBe(2);
expect(oilChange?.percentage).toBeCloseTo(71.7, 1); // 380/530 * 100
const inspection = result.find(r => r.category === 'فحص دوري');
expect(inspection?.amount).toBe(150.00);
expect(inspection?.count).toBe(1);
expect(inspection?.percentage).toBeCloseTo(28.3, 1); // 150/530 * 100
});
});
describe('getExpenseBreakdown', () => {
beforeEach(async () => {
await prisma.expense.createMany({
data: [
{ description: 'قطع غيار 1', category: 'قطع غيار', amount: 100 },
{ description: 'قطع غيار 2', category: 'قطع غيار', amount: 150 },
{ description: 'أدوات 1', category: 'أدوات', amount: 200 },
{ description: 'إيجار', category: 'إيجار', amount: 3000 },
],
});
});
it('should group expenses by category with percentages', async () => {
const result = await getExpenseBreakdown();
expect(result).toHaveLength(3);
const rent = result.find(r => r.category === 'إيجار');
expect(rent?.amount).toBe(3000);
expect(rent?.count).toBe(1);
expect(rent?.percentage).toBeCloseTo(86.96, 1); // 3000/3450 * 100
const spareParts = result.find(r => r.category === 'قطع غيار');
expect(spareParts?.amount).toBe(250);
expect(spareParts?.count).toBe(2);
expect(spareParts?.percentage).toBeCloseTo(7.25, 1); // 250/3450 * 100
const tools = result.find(r => r.category === 'أدوات');
expect(tools?.amount).toBe(200);
expect(tools?.count).toBe(1);
expect(tools?.percentage).toBeCloseTo(5.80, 1); // 200/3450 * 100
});
});
describe('getTopCustomersByRevenue', () => {
beforeEach(async () => {
// Create test customers
const customer1 = await prisma.customer.create({
data: { name: 'عميل أول', phone: '0501111111' },
});
const customer2 = await prisma.customer.create({
data: { name: 'عميل ثاني', phone: '0502222222' },
});
// Create vehicles
const vehicle1 = await prisma.vehicle.create({
data: {
plateNumber: 'ABC-111',
bodyType: 'سيدان',
manufacturer: 'تويوتا',
model: 'كامري',
year: 2020,
transmission: 'Automatic',
fuel: 'Gasoline',
useType: 'personal',
ownerId: customer1.id,
},
});
const vehicle2 = await prisma.vehicle.create({
data: {
plateNumber: 'ABC-222',
bodyType: 'SUV',
manufacturer: 'هيونداي',
model: 'توسان',
year: 2021,
transmission: 'Automatic',
fuel: 'Gasoline',
useType: 'personal',
ownerId: customer2.id,
},
});
// Create maintenance visits
const visit1 = await prisma.maintenanceVisit.create({
data: {
vehicleId: vehicle1.id,
customerId: customer1.id,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت',
cost: 300.00,
kilometers: 50000,
nextVisitDelay: 3,
},
});
const visit2 = await prisma.maintenanceVisit.create({
data: {
vehicleId: vehicle1.id,
customerId: customer1.id,
maintenanceType: 'فحص دوري',
description: 'فحص دوري',
cost: 200.00,
kilometers: 51000,
nextVisitDelay: 3,
},
});
const visit3 = await prisma.maintenanceVisit.create({
data: {
vehicleId: vehicle2.id,
customerId: customer2.id,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت',
cost: 150.00,
kilometers: 30000,
nextVisitDelay: 3,
},
});
// Create income records
await prisma.income.createMany({
data: [
{ maintenanceVisitId: visit1.id, amount: 300.00 },
{ maintenanceVisitId: visit2.id, amount: 200.00 },
{ maintenanceVisitId: visit3.id, amount: 150.00 },
],
});
});
it('should return top customers by revenue', async () => {
const result = await getTopCustomersByRevenue(10);
expect(result).toHaveLength(2);
// Should be sorted by revenue descending
expect(result[0].customerName).toBe('عميل أول');
expect(result[0].totalRevenue).toBe(500.00);
expect(result[0].visitCount).toBe(2);
expect(result[1].customerName).toBe('عميل ثاني');
expect(result[1].totalRevenue).toBe(150.00);
expect(result[1].visitCount).toBe(1);
});
it('should limit results correctly', async () => {
const result = await getTopCustomersByRevenue(1);
expect(result).toHaveLength(1);
expect(result[0].customerName).toBe('عميل أول');
});
});
describe('getFinancialTrends', () => {
beforeEach(async () => {
// Create test customer and vehicle
const customer = await prisma.customer.create({
data: { name: 'عميل تجريبي', phone: '0501234567' },
});
const vehicle = await prisma.vehicle.create({
data: {
plateNumber: 'ABC-123',
bodyType: 'سيدان',
manufacturer: 'تويوتا',
model: 'كامري',
year: 2020,
transmission: 'Automatic',
fuel: 'Gasoline',
useType: 'personal',
ownerId: customer.id,
},
});
// Create maintenance visits for different periods
const currentVisit = await prisma.maintenanceVisit.create({
data: {
vehicleId: vehicle.id,
customerId: customer.id,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت حالي',
cost: 300.00,
kilometers: 50000,
nextVisitDelay: 3,
},
});
const previousVisit = await prisma.maintenanceVisit.create({
data: {
vehicleId: vehicle.id,
customerId: customer.id,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت سابق',
cost: 200.00,
kilometers: 45000,
nextVisitDelay: 3,
},
});
// Create income for current period (Jan 2024)
await prisma.income.create({
data: {
maintenanceVisitId: currentVisit.id,
amount: 300.00,
incomeDate: new Date('2024-01-15'),
},
});
// Create income for previous period (Dec 2023)
await prisma.income.create({
data: {
maintenanceVisitId: previousVisit.id,
amount: 200.00,
incomeDate: new Date('2023-12-15'),
},
});
// Create expenses for current period
await prisma.expense.create({
data: {
description: 'مصروف حالي',
category: 'قطع غيار',
amount: 100.00,
expenseDate: new Date('2024-01-10'),
},
});
// Create expenses for previous period
await prisma.expense.create({
data: {
description: 'مصروف سابق',
category: 'قطع غيار',
amount: 80.00,
expenseDate: new Date('2023-12-10'),
},
});
});
it('should calculate financial trends correctly', async () => {
const dateFrom = new Date('2024-01-01');
const dateTo = new Date('2024-01-31');
const result = await getFinancialTrends(dateFrom, dateTo);
expect(result.currentPeriod.totalIncome).toBe(300.00);
expect(result.currentPeriod.totalExpenses).toBe(100.00);
expect(result.currentPeriod.netProfit).toBe(200.00);
expect(result.previousPeriod.totalIncome).toBe(200.00);
expect(result.previousPeriod.totalExpenses).toBe(80.00);
expect(result.previousPeriod.netProfit).toBe(120.00);
// Income growth: (300-200)/200 * 100 = 50%
expect(result.trends.incomeGrowth).toBeCloseTo(50.0, 1);
// Expense growth: (100-80)/80 * 100 = 25%
expect(result.trends.expenseGrowth).toBeCloseTo(25.0, 1);
// Profit growth: (200-120)/120 * 100 = 66.67%
expect(result.trends.profitGrowth).toBeCloseTo(66.67, 1);
});
});
});

View File

@@ -0,0 +1,201 @@
import { describe, it, expect } from 'vitest';
import {
validateUserData,
validateCustomerData,
validateVehicleData,
validateMaintenanceVisitData,
validateExpenseData
} from '../form-validation';
describe('Form Validation', () => {
describe('validateUserData', () => {
it('should validate valid user data', () => {
const validData = {
name: 'أحمد محمد',
username: 'ahmed123',
email: 'ahmed@example.com',
password: 'password123',
authLevel: 3,
status: 'active',
};
const result = validateUserData(validData);
expect(result.success).toBe(true);
expect(result.errors).toEqual({});
});
it('should return errors for invalid user data', () => {
const invalidData = {
name: '',
username: 'ab',
email: 'invalid-email',
password: '123',
authLevel: 5,
status: 'invalid',
};
const result = validateUserData(invalidData);
expect(result.success).toBe(false);
expect(result.errors.name).toBe('الاسم مطلوب');
expect(result.errors.username).toBe('اسم المستخدم يجب أن يكون على الأقل 3 أحرف');
expect(result.errors.email).toBe('البريد الإلكتروني غير صحيح');
expect(result.errors.password).toBe('كلمة المرور يجب أن تكون على الأقل 6 أحرف');
});
});
describe('validateCustomerData', () => {
it('should validate valid customer data', () => {
const validData = {
name: 'محمد أحمد',
phone: '+966501234567',
email: 'mohammed@example.com',
address: 'الرياض، المملكة العربية السعودية',
};
const result = validateCustomerData(validData);
expect(result.success).toBe(true);
expect(result.errors).toEqual({});
});
it('should validate customer with minimal data', () => {
const validData = {
name: 'سارة علي',
phone: '',
email: '',
address: '',
};
const result = validateCustomerData(validData);
expect(result.success).toBe(true);
expect(result.errors).toEqual({});
});
it('should return errors for invalid customer data', () => {
const invalidData = {
name: '',
phone: 'invalid-phone',
email: 'invalid-email',
address: 'Valid address',
};
const result = validateCustomerData(invalidData);
expect(result.success).toBe(false);
expect(result.errors.name).toBe('اسم العميل مطلوب');
expect(result.errors.phone).toBe('رقم الهاتف غير صحيح');
expect(result.errors.email).toBe('البريد الإلكتروني غير صحيح');
});
});
describe('validateVehicleData', () => {
it('should validate valid vehicle data', () => {
const validData = {
plateNumber: 'ABC-1234',
bodyType: 'Sedan',
manufacturer: 'Toyota',
model: 'Camry',
trim: 'LE',
year: '2023',
transmission: 'Automatic',
fuel: 'Gasoline',
cylinders: '4',
engineDisplacement: '2.5',
useType: 'personal',
ownerId: '1',
};
const result = validateVehicleData(validData);
expect(result.success).toBe(true);
expect(result.errors).toEqual({});
});
it('should return errors for invalid vehicle data', () => {
const invalidData = {
plateNumber: '',
bodyType: '',
manufacturer: '',
model: '',
year: '1800',
transmission: 'invalid',
fuel: 'invalid',
cylinders: '20',
engineDisplacement: '50',
useType: 'invalid',
ownerId: '0',
};
const result = validateVehicleData(invalidData);
expect(result.success).toBe(false);
expect(result.errors.plateNumber).toBe('رقم اللوحة مطلوب');
expect(result.errors.bodyType).toBe('نوع الهيكل مطلوب');
expect(result.errors.manufacturer).toBe('الشركة المصنعة مطلوبة');
expect(result.errors.model).toBe('الموديل مطلوب');
expect(result.errors.ownerId).toBe('مالك المركبة مطلوب');
});
});
describe('validateMaintenanceVisitData', () => {
it('should validate valid maintenance visit data', () => {
const validData = {
vehicleId: '1',
customerId: '1',
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك والفلتر',
cost: '150.50',
paymentStatus: 'paid',
kilometers: '50000',
nextVisitDelay: '3',
};
const result = validateMaintenanceVisitData(validData);
expect(result.success).toBe(true);
expect(result.errors).toEqual({});
});
it('should return errors for invalid maintenance visit data', () => {
const invalidData = {
vehicleId: '0',
customerId: '0',
maintenanceType: '',
description: '',
cost: '-10',
paymentStatus: 'invalid',
kilometers: '-100',
nextVisitDelay: '5',
};
const result = validateMaintenanceVisitData(invalidData);
expect(result.success).toBe(false);
expect(result.errors.vehicleId).toBe('المركبة مطلوبة');
expect(result.errors.customerId).toBe('العميل مطلوب');
expect(result.errors.maintenanceType).toBe('نوع الصيانة مطلوب');
expect(result.errors.description).toBe('وصف الصيانة مطلوب');
});
});
describe('validateExpenseData', () => {
it('should validate valid expense data', () => {
const validData = {
description: 'شراء قطع غيار',
category: 'قطع غيار',
amount: '250.75',
};
const result = validateExpenseData(validData);
expect(result.success).toBe(true);
expect(result.errors).toEqual({});
});
it('should return errors for invalid expense data', () => {
const invalidData = {
description: '',
category: '',
amount: '0',
};
const result = validateExpenseData(invalidData);
expect(result.success).toBe(false);
expect(result.errors.description).toBe('وصف المصروف مطلوب');
expect(result.errors.category).toBe('فئة المصروف مطلوبة');
});
});
});

View File

@@ -0,0 +1,434 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
createMaintenanceVisit,
getMaintenanceVisits,
getMaintenanceVisitById,
updateMaintenanceVisit,
deleteMaintenanceVisit,
getVehicleMaintenanceHistory,
getCustomerMaintenanceHistory
} from '../maintenance-visit-management.server';
import { prisma } from '../db.server';
import type { Customer, Vehicle } from '@prisma/client';
describe('Maintenance Visit Management', () => {
let testCustomer: Customer;
let testVehicle: Vehicle;
beforeEach(async () => {
// Create test customer
testCustomer = await prisma.customer.create({
data: {
name: 'أحمد محمد',
phone: '0501234567',
email: 'ahmed@example.com',
},
});
// Create test vehicle
testVehicle = await prisma.vehicle.create({
data: {
plateNumber: 'أ ب ج 123',
bodyType: 'سيدان',
manufacturer: 'تويوتا',
model: 'كامري',
year: 2020,
transmission: 'Automatic',
fuel: 'Gasoline',
useType: 'personal',
ownerId: testCustomer.id,
},
});
});
afterEach(async () => {
// Clean up test data
await prisma.income.deleteMany();
await prisma.maintenanceVisit.deleteMany();
await prisma.vehicle.deleteMany();
await prisma.customer.deleteMany();
});
describe('createMaintenanceVisit', () => {
it('should create a maintenance visit successfully', async () => {
const visitData = {
vehicleId: testVehicle.id,
customerId: testCustomer.id,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك والفلتر',
cost: 150.00,
paymentStatus: 'paid',
kilometers: 50000,
nextVisitDelay: 3,
};
const visit = await createMaintenanceVisit(visitData);
expect(visit).toBeDefined();
expect(visit.vehicleId).toBe(testVehicle.id);
expect(visit.customerId).toBe(testCustomer.id);
expect(visit.maintenanceType).toBe('تغيير زيت');
expect(visit.cost).toBe(150.00);
expect(visit.nextVisitDelay).toBe(3);
// Check that vehicle was updated
const updatedVehicle = await prisma.vehicle.findUnique({
where: { id: testVehicle.id },
});
expect(updatedVehicle?.lastVisitDate).toBeDefined();
expect(updatedVehicle?.suggestedNextVisitDate).toBeDefined();
// Check that income was created
const income = await prisma.income.findFirst({
where: { maintenanceVisitId: visit.id },
});
expect(income).toBeDefined();
expect(income?.amount).toBe(150.00);
});
it('should calculate next visit date correctly', async () => {
const visitData = {
vehicleId: testVehicle.id,
customerId: testCustomer.id,
maintenanceType: 'فحص دوري',
description: 'فحص دوري شامل',
cost: 200.00,
kilometers: 45000,
nextVisitDelay: 2, // 2 months
};
await createMaintenanceVisit(visitData);
const updatedVehicle = await prisma.vehicle.findUnique({
where: { id: testVehicle.id },
});
expect(updatedVehicle?.suggestedNextVisitDate).toBeDefined();
// Check that the suggested date is approximately 2 months from now
const now = new Date();
const expectedDate = new Date();
expectedDate.setMonth(expectedDate.getMonth() + 2);
const actualDate = updatedVehicle!.suggestedNextVisitDate!;
const timeDiff = Math.abs(actualDate.getTime() - expectedDate.getTime());
const daysDiff = timeDiff / (1000 * 60 * 60 * 24);
expect(daysDiff).toBeLessThan(2); // Allow 2 days difference
});
});
describe('getMaintenanceVisits', () => {
beforeEach(async () => {
// Create test visits
await createMaintenanceVisit({
vehicleId: testVehicle.id,
customerId: testCustomer.id,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 150.00,
kilometers: 50000,
nextVisitDelay: 3,
});
await createMaintenanceVisit({
vehicleId: testVehicle.id,
customerId: testCustomer.id,
maintenanceType: 'فحص فرامل',
description: 'فحص وتنظيف الفرامل',
cost: 100.00,
kilometers: 52000,
nextVisitDelay: 2,
});
});
it('should get all maintenance visits', async () => {
const result = await getMaintenanceVisits();
expect(result.visits).toHaveLength(2);
expect(result.total).toBe(2);
expect(result.totalPages).toBe(1);
});
it('should search maintenance visits by maintenance type', async () => {
const result = await getMaintenanceVisits('تغيير زيت');
expect(result.visits).toHaveLength(1);
expect(result.visits[0].maintenanceType).toBe('تغيير زيت');
});
it('should filter by vehicle ID', async () => {
const result = await getMaintenanceVisits('', 1, 10, testVehicle.id);
expect(result.visits).toHaveLength(2);
result.visits.forEach(visit => {
expect(visit.vehicleId).toBe(testVehicle.id);
});
});
it('should filter by customer ID', async () => {
const result = await getMaintenanceVisits('', 1, 10, undefined, testCustomer.id);
expect(result.visits).toHaveLength(2);
result.visits.forEach(visit => {
expect(visit.customerId).toBe(testCustomer.id);
});
});
it('should paginate results', async () => {
const result = await getMaintenanceVisits('', 1, 1);
expect(result.visits).toHaveLength(1);
expect(result.total).toBe(2);
expect(result.totalPages).toBe(2);
});
});
describe('getMaintenanceVisitById', () => {
it('should get a maintenance visit by ID', async () => {
const createdVisit = await createMaintenanceVisit({
vehicleId: testVehicle.id,
customerId: testCustomer.id,
maintenanceType: 'تغيير إطارات',
description: 'تغيير الإطارات الأمامية',
cost: 800.00,
kilometers: 55000,
nextVisitDelay: 4,
});
const visit = await getMaintenanceVisitById(createdVisit.id);
expect(visit).toBeDefined();
expect(visit?.id).toBe(createdVisit.id);
expect(visit?.vehicle).toBeDefined();
expect(visit?.customer).toBeDefined();
expect(visit?.income).toBeDefined();
});
it('should return null for non-existent visit', async () => {
const visit = await getMaintenanceVisitById(99999);
expect(visit).toBeNull();
});
});
describe('updateMaintenanceVisit', () => {
it('should update a maintenance visit successfully', async () => {
const createdVisit = await createMaintenanceVisit({
vehicleId: testVehicle.id,
customerId: testCustomer.id,
maintenanceType: 'فحص دوري',
description: 'فحص دوري أولي',
cost: 200.00,
kilometers: 48000,
nextVisitDelay: 3,
});
const updateData = {
maintenanceType: 'فحص شامل',
description: 'فحص شامل محدث',
cost: 250.00,
paymentStatus: 'paid',
kilometers: 48500,
nextVisitDelay: 2,
};
const updatedVisit = await updateMaintenanceVisit(createdVisit.id, updateData);
expect(updatedVisit.maintenanceType).toBe('فحص شامل');
expect(updatedVisit.description).toBe('فحص شامل محدث');
expect(updatedVisit.cost).toBe(250.00);
expect(updatedVisit.paymentStatus).toBe('paid');
expect(updatedVisit.kilometers).toBe(48500);
expect(updatedVisit.nextVisitDelay).toBe(2);
// Check that income was updated
const income = await prisma.income.findFirst({
where: { maintenanceVisitId: createdVisit.id },
});
expect(income?.amount).toBe(250.00);
});
it('should update vehicle dates when visit date changes', async () => {
const createdVisit = await createMaintenanceVisit({
vehicleId: testVehicle.id,
customerId: testCustomer.id,
maintenanceType: 'صيانة عامة',
description: 'صيانة عامة',
cost: 300.00,
kilometers: 50000,
nextVisitDelay: 3,
});
const newVisitDate = new Date();
newVisitDate.setDate(newVisitDate.getDate() - 5); // 5 days ago
await updateMaintenanceVisit(createdVisit.id, {
visitDate: newVisitDate,
nextVisitDelay: 4,
});
const updatedVehicle = await prisma.vehicle.findUnique({
where: { id: testVehicle.id },
});
expect(updatedVehicle?.lastVisitDate?.getTime()).toBe(newVisitDate.getTime());
// Check that suggested next visit date was recalculated
const expectedNextDate = new Date(newVisitDate);
expectedNextDate.setMonth(expectedNextDate.getMonth() + 4);
const actualNextDate = updatedVehicle!.suggestedNextVisitDate!;
const timeDiff = Math.abs(actualNextDate.getTime() - expectedNextDate.getTime());
const daysDiff = timeDiff / (1000 * 60 * 60 * 24);
expect(daysDiff).toBeLessThan(2);
});
});
describe('deleteMaintenanceVisit', () => {
it('should delete a maintenance visit successfully', async () => {
const createdVisit = await createMaintenanceVisit({
vehicleId: testVehicle.id,
customerId: testCustomer.id,
maintenanceType: 'تنظيف مكيف',
description: 'تنظيف وصيانة المكيف',
cost: 120.00,
kilometers: 47000,
nextVisitDelay: 2,
});
await deleteMaintenanceVisit(createdVisit.id);
// Check that visit was deleted
const deletedVisit = await prisma.maintenanceVisit.findUnique({
where: { id: createdVisit.id },
});
expect(deletedVisit).toBeNull();
// Check that income was deleted (cascade)
const income = await prisma.income.findFirst({
where: { maintenanceVisitId: createdVisit.id },
});
expect(income).toBeNull();
// Check that vehicle dates were cleared
const updatedVehicle = await prisma.vehicle.findUnique({
where: { id: testVehicle.id },
});
expect(updatedVehicle?.lastVisitDate).toBeNull();
expect(updatedVehicle?.suggestedNextVisitDate).toBeNull();
});
it('should update vehicle dates to most recent remaining visit after deletion', async () => {
// Create two visits
const firstVisit = await createMaintenanceVisit({
vehicleId: testVehicle.id,
customerId: testCustomer.id,
maintenanceType: 'زيت قديم',
description: 'تغيير زيت قديم',
cost: 100.00,
kilometers: 45000,
nextVisitDelay: 3,
visitDate: new Date('2023-01-01'),
});
const secondVisit = await createMaintenanceVisit({
vehicleId: testVehicle.id,
customerId: testCustomer.id,
maintenanceType: 'زيت جديد',
description: 'تغيير زيت جديد',
cost: 150.00,
kilometers: 50000,
nextVisitDelay: 2,
visitDate: new Date('2023-02-01'),
});
// Delete the more recent visit
await deleteMaintenanceVisit(secondVisit.id);
// Check that vehicle dates were updated to the remaining visit
const updatedVehicle = await prisma.vehicle.findUnique({
where: { id: testVehicle.id },
});
expect(updatedVehicle?.lastVisitDate?.getTime()).toBe(new Date('2023-01-01').getTime());
// Check that suggested next visit date was recalculated based on first visit
const expectedNextDate = new Date('2023-01-01');
expectedNextDate.setMonth(expectedNextDate.getMonth() + 3);
const actualNextDate = updatedVehicle!.suggestedNextVisitDate!;
expect(actualNextDate.getTime()).toBe(expectedNextDate.getTime());
});
});
describe('getVehicleMaintenanceHistory', () => {
it('should get maintenance history for a vehicle', async () => {
await createMaintenanceVisit({
vehicleId: testVehicle.id,
customerId: testCustomer.id,
maintenanceType: 'زيت أول',
description: 'تغيير زيت أول',
cost: 100.00,
kilometers: 45000,
nextVisitDelay: 3,
});
await createMaintenanceVisit({
vehicleId: testVehicle.id,
customerId: testCustomer.id,
maintenanceType: 'زيت ثاني',
description: 'تغيير زيت ثاني',
cost: 120.00,
kilometers: 48000,
nextVisitDelay: 3,
});
const history = await getVehicleMaintenanceHistory(testVehicle.id);
expect(history).toHaveLength(2);
expect(history[0].vehicleId).toBe(testVehicle.id);
expect(history[1].vehicleId).toBe(testVehicle.id);
// Should be ordered by visit date descending
expect(new Date(history[0].visitDate).getTime()).toBeGreaterThanOrEqual(
new Date(history[1].visitDate).getTime()
);
});
});
describe('getCustomerMaintenanceHistory', () => {
it('should get maintenance history for a customer', async () => {
await createMaintenanceVisit({
vehicleId: testVehicle.id,
customerId: testCustomer.id,
maintenanceType: 'صيانة أولى',
description: 'صيانة أولى',
cost: 200.00,
kilometers: 45000,
nextVisitDelay: 3,
});
await createMaintenanceVisit({
vehicleId: testVehicle.id,
customerId: testCustomer.id,
maintenanceType: 'صيانة ثانية',
description: 'صيانة ثانية',
cost: 250.00,
kilometers: 48000,
nextVisitDelay: 2,
});
const history = await getCustomerMaintenanceHistory(testCustomer.id);
expect(history).toHaveLength(2);
expect(history[0].customerId).toBe(testCustomer.id);
expect(history[1].customerId).toBe(testCustomer.id);
// Should be ordered by visit date descending
expect(new Date(history[0].visitDate).getTime()).toBeGreaterThanOrEqual(
new Date(history[1].visitDate).getTime()
);
});
});
});

View File

@@ -0,0 +1,425 @@
import { describe, it, expect } from 'vitest';
import { validateMaintenanceVisit } from '../validation';
describe('Maintenance Visit Validation', () => {
describe('validateMaintenanceVisit', () => {
it('should validate a complete maintenance visit successfully', () => {
const validData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك والفلتر',
cost: 150.50,
paymentStatus: 'paid',
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(validData);
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
it('should skip validation for missing vehicleId (partial validation)', () => {
const partialData = {
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 150.00,
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(partialData);
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
it('should reject invalid vehicleId', () => {
const invalidData = {
vehicleId: 0,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 150.00,
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(invalidData);
expect(result.isValid).toBe(false);
expect(result.errors.vehicleId).toBe('المركبة مطلوبة');
});
it('should skip validation for missing customerId (partial validation)', () => {
const partialData = {
vehicleId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 150.00,
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(partialData);
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
it('should reject invalid customerId', () => {
const invalidData = {
vehicleId: 1,
customerId: -1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 150.00,
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(invalidData);
expect(result.isValid).toBe(false);
expect(result.errors.customerId).toBe('العميل مطلوب');
});
it('should skip validation for missing maintenanceType (partial validation)', () => {
const partialData = {
vehicleId: 1,
customerId: 1,
description: 'تغيير زيت المحرك',
cost: 150.00,
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(partialData);
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
it('should reject empty maintenanceType', () => {
const invalidData = {
vehicleId: 1,
customerId: 1,
maintenanceType: ' ',
description: 'تغيير زيت المحرك',
cost: 150.00,
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(invalidData);
expect(result.isValid).toBe(false);
expect(result.errors.maintenanceType).toBe('نوع الصيانة مطلوب');
});
it('should skip validation for missing description (partial validation)', () => {
const partialData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
cost: 150.00,
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(partialData);
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
it('should reject empty description', () => {
const invalidData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: '',
cost: 150.00,
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(invalidData);
expect(result.isValid).toBe(false);
expect(result.errors.description).toBe('وصف الصيانة مطلوب');
});
it('should reject description that is too long', () => {
const longDescription = 'أ'.repeat(501); // 501 characters
const invalidData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: longDescription,
cost: 150.00,
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(invalidData);
expect(result.isValid).toBe(false);
expect(result.errors.description).toBe('الوصف يجب أن يكون أقل من 500 حرف');
});
it('should skip validation for missing cost (partial validation)', () => {
const partialData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(partialData);
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
it('should reject negative cost', () => {
const invalidData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: -10.00,
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(invalidData);
expect(result.isValid).toBe(false);
expect(result.errors.cost).toBe('التكلفة يجب أن تكون بين 0 و 999999.99');
});
it('should reject cost that is too high', () => {
const invalidData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 1000000.00,
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(invalidData);
expect(result.isValid).toBe(false);
expect(result.errors.cost).toBe('التكلفة يجب أن تكون بين 0 و 999999.99');
});
it('should validate payment status', () => {
const invalidData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 150.00,
paymentStatus: 'invalid_status',
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(invalidData);
expect(result.isValid).toBe(false);
expect(result.errors.paymentStatus).toBe('حالة الدفع غير صحيحة');
});
it('should accept valid payment statuses', () => {
const validStatuses = ['pending', 'paid', 'partial', 'cancelled'];
validStatuses.forEach(status => {
const validData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 150.00,
paymentStatus: status,
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(validData);
expect(result.isValid).toBe(true);
});
});
it('should skip validation for missing kilometers (partial validation)', () => {
const partialData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 150.00,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(partialData);
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
it('should reject negative kilometers', () => {
const invalidData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 150.00,
kilometers: -1000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(invalidData);
expect(result.isValid).toBe(false);
expect(result.errors.kilometers).toBe('عدد الكيلومترات يجب أن يكون رقم موجب');
});
it('should accept zero kilometers', () => {
const validData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 150.00,
kilometers: 0,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(validData);
expect(result.isValid).toBe(true);
});
it('should skip validation for missing nextVisitDelay (partial validation)', () => {
const partialData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 150.00,
kilometers: 50000,
};
const result = validateMaintenanceVisit(partialData);
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
it('should accept valid nextVisitDelay values', () => {
const validDelays = [1, 2, 3, 4];
validDelays.forEach(delay => {
const validData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 150.00,
kilometers: 50000,
nextVisitDelay: delay,
};
const result = validateMaintenanceVisit(validData);
expect(result.isValid).toBe(true);
});
});
it('should reject invalid nextVisitDelay values', () => {
const invalidDelays = [0, 5, 6, -1];
invalidDelays.forEach(delay => {
const invalidData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 150.00,
kilometers: 50000,
nextVisitDelay: delay,
};
const result = validateMaintenanceVisit(invalidData);
expect(result.isValid).toBe(false);
expect(result.errors.nextVisitDelay).toBe('فترة الزيارة التالية يجب أن تكون 1، 2، 3، أو 4 أشهر');
});
});
it('should handle partial validation for updates', () => {
const partialData = {
maintenanceType: 'فحص دوري',
cost: 200.00,
};
const result = validateMaintenanceVisit(partialData);
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
it('should validate partial data with errors', () => {
const partialData = {
maintenanceType: '',
cost: -50.00,
paymentStatus: 'invalid',
};
const result = validateMaintenanceVisit(partialData);
expect(result.isValid).toBe(false);
expect(result.errors.maintenanceType).toBe('نوع الصيانة مطلوب');
expect(result.errors.cost).toBe('التكلفة يجب أن تكون بين 0 و 999999.99');
expect(result.errors.paymentStatus).toBe('حالة الدفع غير صحيحة');
});
it('should accept zero cost', () => {
const validData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'فحص مجاني',
description: 'فحص مجاني للعميل',
cost: 0,
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(validData);
expect(result.isValid).toBe(true);
});
it('should accept decimal cost values', () => {
const validData = {
vehicleId: 1,
customerId: 1,
maintenanceType: 'تغيير زيت',
description: 'تغيير زيت المحرك',
cost: 150.75,
kilometers: 50000,
nextVisitDelay: 3,
};
const result = validateMaintenanceVisit(validData);
expect(result.isValid).toBe(true);
});
});
});

View File

@@ -0,0 +1,107 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { checkPermission, createUnauthorizedResponse } from "../auth-middleware.server";
import { AUTH_LEVELS, USER_STATUS } from "~/types/auth";
import type { SafeUser } from "~/types/auth";
// Mock user data for testing permissions
const mockSuperAdmin: SafeUser = {
id: 1,
name: "Super Admin",
username: "superadmin",
email: "super@example.com",
status: USER_STATUS.ACTIVE,
authLevel: AUTH_LEVELS.SUPERADMIN,
createdDate: new Date(),
editDate: new Date(),
};
const mockAdmin: SafeUser = {
id: 2,
name: "Admin User",
username: "admin",
email: "admin@example.com",
status: USER_STATUS.ACTIVE,
authLevel: AUTH_LEVELS.ADMIN,
createdDate: new Date(),
editDate: new Date(),
};
const mockUser: SafeUser = {
id: 3,
name: "Regular User",
username: "user",
email: "user@example.com",
status: USER_STATUS.ACTIVE,
authLevel: AUTH_LEVELS.USER,
createdDate: new Date(),
editDate: new Date(),
};
describe("Route Protection Integration Tests", () => {
describe("checkPermission", () => {
it("should correctly check view_all_users permission", () => {
expect(checkPermission(mockSuperAdmin, "view_all_users")).toBe(true);
expect(checkPermission(mockAdmin, "view_all_users")).toBe(false);
expect(checkPermission(mockUser, "view_all_users")).toBe(false);
});
it("should correctly check create_users permission", () => {
expect(checkPermission(mockSuperAdmin, "create_users")).toBe(true);
expect(checkPermission(mockAdmin, "create_users")).toBe(true);
expect(checkPermission(mockUser, "create_users")).toBe(false);
});
it("should correctly check manage_finances permission", () => {
expect(checkPermission(mockSuperAdmin, "manage_finances")).toBe(true);
expect(checkPermission(mockAdmin, "manage_finances")).toBe(true);
expect(checkPermission(mockUser, "manage_finances")).toBe(false);
});
it("should correctly check view_reports permission", () => {
expect(checkPermission(mockSuperAdmin, "view_reports")).toBe(true);
expect(checkPermission(mockAdmin, "view_reports")).toBe(true);
expect(checkPermission(mockUser, "view_reports")).toBe(false);
});
it("should return false for unknown permission", () => {
expect(checkPermission(mockUser, "unknown_permission" as any)).toBe(false);
expect(checkPermission(mockAdmin, "unknown_permission" as any)).toBe(false);
expect(checkPermission(mockSuperAdmin, "unknown_permission" as any)).toBe(false);
});
});
describe("createUnauthorizedResponse", () => {
it("should create response with default message", () => {
const response = createUnauthorizedResponse();
expect(response.status).toBe(403);
expect(response.headers.get("Content-Type")).toBe("text/plain; charset=utf-8");
});
it("should create response with custom message", () => {
const customMessage = "Custom error message";
const response = createUnauthorizedResponse(customMessage);
expect(response.status).toBe(403);
expect(response.headers.get("Content-Type")).toBe("text/plain; charset=utf-8");
});
});
describe("Auth Level Hierarchy", () => {
it("should have correct auth level values", () => {
expect(AUTH_LEVELS.SUPERADMIN).toBe(1);
expect(AUTH_LEVELS.ADMIN).toBe(2);
expect(AUTH_LEVELS.USER).toBe(3);
});
it("should enforce correct hierarchy (lower number = higher privilege)", () => {
expect(AUTH_LEVELS.SUPERADMIN < AUTH_LEVELS.ADMIN).toBe(true);
expect(AUTH_LEVELS.ADMIN < AUTH_LEVELS.USER).toBe(true);
});
});
describe("User Status", () => {
it("should have correct status values", () => {
expect(USER_STATUS.ACTIVE).toBe("active");
expect(USER_STATUS.INACTIVE).toBe("inactive");
});
});
});

View File

@@ -0,0 +1,299 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { prisma } from '../db.server';
import { getUsers, createUser, updateUser, deleteUser, toggleUserStatus } from '../user-management.server';
import { hashPassword } from '../auth.server';
import { AUTH_LEVELS, USER_STATUS } from '~/types/auth';
describe('User Management', () => {
beforeEach(async () => {
// Clean up test data
await prisma.user.deleteMany({
where: {
email: {
contains: 'test'
}
}
});
// Ensure the main superadmin exists for login functionality
const existingSuperadmin = await prisma.user.findUnique({
where: { username: 'superadmin' }
});
if (!existingSuperadmin) {
const hashedPassword = await hashPassword('admin123');
await prisma.user.create({
data: {
name: 'Super Administrator',
username: 'superadmin',
email: 'admin@carmaintenance.com',
password: hashedPassword,
status: USER_STATUS.ACTIVE,
authLevel: AUTH_LEVELS.SUPERADMIN,
},
});
}
});
afterEach(async () => {
// Clean up test data but preserve the main superadmin
await prisma.user.deleteMany({
where: {
AND: [
{
email: {
contains: 'test'
}
},
{
username: {
not: 'superadmin'
}
}
]
}
});
});
describe('getUsers', () => {
it('should return users with role-based filtering', async () => {
// Create test users with properly hashed passwords
const hashedPassword = await hashPassword('testpassword123');
const superadmin = await prisma.user.create({
data: {
name: 'Test Superadmin',
username: 'testsuperadmin',
email: 'testsuperadmin@example.com',
password: hashedPassword,
authLevel: AUTH_LEVELS.SUPERADMIN,
status: USER_STATUS.ACTIVE,
}
});
const admin = await prisma.user.create({
data: {
name: 'Test Admin',
username: 'testadmin',
email: 'testadmin@example.com',
password: hashedPassword,
authLevel: AUTH_LEVELS.ADMIN,
status: USER_STATUS.ACTIVE,
}
});
// Superadmin should see all users
const superadminResult = await getUsers(AUTH_LEVELS.SUPERADMIN);
expect(superadminResult.users.length).toBeGreaterThanOrEqual(2);
// Admin should not see superadmin users
const adminResult = await getUsers(AUTH_LEVELS.ADMIN);
const adminVisibleUsers = adminResult.users.filter(u => u.authLevel === AUTH_LEVELS.SUPERADMIN);
expect(adminVisibleUsers.length).toBe(0);
});
it('should support search functionality', async () => {
const hashedPassword = await hashPassword('testpassword123');
await prisma.user.create({
data: {
name: 'John Test User',
username: 'johntestuser',
email: 'johntest@example.com',
password: hashedPassword,
authLevel: AUTH_LEVELS.USER,
status: USER_STATUS.ACTIVE,
}
});
const result = await getUsers(AUTH_LEVELS.SUPERADMIN, 'John');
expect(result.users.some(u => u.name.includes('John'))).toBe(true);
});
});
describe('createUser', () => {
it('should create a new user successfully', async () => {
const userData = {
name: 'New Test User',
username: 'newtestuser',
email: 'newtest@example.com',
password: 'password123',
authLevel: AUTH_LEVELS.USER,
status: USER_STATUS.ACTIVE,
};
const result = await createUser(userData, AUTH_LEVELS.SUPERADMIN);
expect(result.success).toBe(true);
expect(result.user).toBeDefined();
expect(result.user?.name).toBe(userData.name);
expect(result.user?.username).toBe(userData.username);
expect(result.user?.email).toBe(userData.email);
});
it('should prevent admin from creating superadmin', async () => {
const userData = {
name: 'Test Superadmin',
username: 'testsuperadmin2',
email: 'testsuperadmin2@example.com',
password: 'password123',
authLevel: AUTH_LEVELS.SUPERADMIN,
status: USER_STATUS.ACTIVE,
};
const result = await createUser(userData, AUTH_LEVELS.ADMIN);
expect(result.success).toBe(false);
expect(result.error).toContain('لا يمكن للمدير إنشاء حساب مدير عام');
});
it('should prevent duplicate username', async () => {
const userData1 = {
name: 'Test User 1',
username: 'duplicatetest',
email: 'test1@example.com',
password: 'password123',
authLevel: AUTH_LEVELS.USER,
status: USER_STATUS.ACTIVE,
};
const userData2 = {
name: 'Test User 2',
username: 'duplicatetest', // Same username
email: 'test2@example.com',
password: 'password123',
authLevel: AUTH_LEVELS.USER,
status: USER_STATUS.ACTIVE,
};
await createUser(userData1, AUTH_LEVELS.SUPERADMIN);
const result = await createUser(userData2, AUTH_LEVELS.SUPERADMIN);
expect(result.success).toBe(false);
expect(result.error).toContain('اسم المستخدم موجود بالفعل');
});
});
describe('updateUser', () => {
it('should update user successfully', async () => {
const hashedPassword = await hashPassword('testpassword123');
const user = await prisma.user.create({
data: {
name: 'Original Name',
username: 'originaluser' + Date.now(),
email: 'original' + Date.now() + '@example.com',
password: hashedPassword,
authLevel: AUTH_LEVELS.USER,
status: USER_STATUS.ACTIVE,
}
});
const updateData = {
name: 'Updated Name',
email: 'updated' + Date.now() + '@example.com',
};
const result = await updateUser(user.id, updateData, AUTH_LEVELS.SUPERADMIN);
if (!result.success) {
console.log('Update failed:', result.error);
}
expect(result.success).toBe(true);
expect(result.user?.name).toBe('Updated Name');
expect(result.user?.email).toBe(updateData.email);
});
});
describe('toggleUserStatus', () => {
it('should toggle user status', async () => {
const hashedPassword = await hashPassword('testpassword123');
const user = await prisma.user.create({
data: {
name: 'Test User',
username: 'teststatususer' + Date.now(),
email: 'teststatus' + Date.now() + '@example.com',
password: hashedPassword,
authLevel: AUTH_LEVELS.USER,
status: USER_STATUS.ACTIVE,
}
});
const result = await toggleUserStatus(user.id, AUTH_LEVELS.SUPERADMIN);
expect(result.success).toBe(true);
expect(result.user?.status).toBe(USER_STATUS.INACTIVE);
});
});
describe('deleteUser', () => {
it('should delete user successfully', async () => {
const hashedPassword = await hashPassword('testpassword123');
const user = await prisma.user.create({
data: {
name: 'Delete Test User',
username: 'deletetestuser' + Date.now(),
email: 'deletetest' + Date.now() + '@example.com',
password: hashedPassword,
authLevel: AUTH_LEVELS.USER,
status: USER_STATUS.ACTIVE,
}
});
const result = await deleteUser(user.id, AUTH_LEVELS.SUPERADMIN);
expect(result.success).toBe(true);
// Verify user is deleted
const deletedUser = await prisma.user.findUnique({
where: { id: user.id }
});
expect(deletedUser).toBeNull();
});
it('should prevent deletion of last superadmin', async () => {
// Delete any test superadmins but keep the main superadmin
await prisma.user.deleteMany({
where: {
AND: [
{ authLevel: AUTH_LEVELS.SUPERADMIN },
{ username: { not: 'superadmin' } }
]
}
});
// Try to delete the main superadmin (should fail as it's the last one)
const mainSuperadmin = await prisma.user.findUnique({
where: { username: 'superadmin' }
});
if (mainSuperadmin) {
const result = await deleteUser(mainSuperadmin.id, AUTH_LEVELS.SUPERADMIN);
expect(result.success).toBe(false);
expect(result.error).toContain('لا يمكن حذف آخر مدير عام في النظام');
} else {
// If main superadmin doesn't exist, create one and test
const hashedPassword = await hashPassword('admin123');
const superadmin = await prisma.user.create({
data: {
name: 'Last Superadmin',
username: 'lastsuperadmin' + Date.now(),
email: 'lastsuperadmin' + Date.now() + '@example.com',
password: hashedPassword,
authLevel: AUTH_LEVELS.SUPERADMIN,
status: USER_STATUS.ACTIVE,
}
});
const result = await deleteUser(superadmin.id, AUTH_LEVELS.SUPERADMIN);
expect(result.success).toBe(false);
expect(result.error).toContain('لا يمكن حذف آخر مدير عام في النظام');
}
});
});
});

View File

@@ -0,0 +1,222 @@
import { describe, it, expect } from 'vitest';
import {
validateField,
validateFields,
validateEmail,
validatePhone,
validatePassword,
validateUsername,
validatePlateNumber,
validateYear,
validateCurrency,
sanitizeString,
sanitizeNumber,
sanitizeInteger,
sanitizeFormData,
PATTERNS
} from '../validation-utils';
describe('Validation Utils', () => {
describe('validateField', () => {
it('should validate required fields', () => {
const result1 = validateField('', { required: true });
expect(result1.isValid).toBe(false);
expect(result1.error).toBe('هذا الحقل مطلوب');
const result2 = validateField('value', { required: true });
expect(result2.isValid).toBe(true);
});
it('should validate email fields', () => {
const result1 = validateField('invalid-email', { email: true });
expect(result1.isValid).toBe(false);
expect(result1.error).toBe('البريد الإلكتروني غير صحيح');
const result2 = validateField('test@example.com', { email: true });
expect(result2.isValid).toBe(true);
});
it('should validate phone fields', () => {
const result1 = validateField('invalid-phone', { phone: true });
expect(result1.isValid).toBe(false);
expect(result1.error).toBe('رقم الهاتف غير صحيح');
const result2 = validateField('+966501234567', { phone: true });
expect(result2.isValid).toBe(true);
});
it('should validate length constraints', () => {
const result1 = validateField('ab', { minLength: 3 });
expect(result1.isValid).toBe(false);
expect(result1.error).toBe('يجب أن يكون على الأقل 3 أحرف');
const result2 = validateField('a'.repeat(101), { maxLength: 100 });
expect(result2.isValid).toBe(false);
expect(result2.error).toBe('يجب أن يكون أقل من 100 حرف');
});
it('should validate numeric constraints', () => {
const result1 = validateField(5, { min: 10 });
expect(result1.isValid).toBe(false);
expect(result1.error).toBe('يجب أن يكون على الأقل 10');
const result2 = validateField(15, { max: 10 });
expect(result2.isValid).toBe(false);
expect(result2.error).toBe('يجب أن يكون أقل من 10');
});
it('should validate custom rules', () => {
const customRule = (value: any) => {
return value === 'forbidden' ? 'هذه القيمة غير مسموحة' : null;
};
const result1 = validateField('forbidden', { custom: customRule });
expect(result1.isValid).toBe(false);
expect(result1.error).toBe('هذه القيمة غير مسموحة');
const result2 = validateField('allowed', { custom: customRule });
expect(result2.isValid).toBe(true);
});
});
describe('validateFields', () => {
it('should validate multiple fields', () => {
const data = {
name: '',
email: 'invalid-email',
age: 5,
};
const rules = {
name: { required: true },
email: { email: true },
age: { min: 18 },
};
const result = validateFields(data, rules);
expect(result.isValid).toBe(false);
expect(result.errors.name).toBe('هذا الحقل مطلوب');
expect(result.errors.email).toBe('البريد الإلكتروني غير صحيح');
expect(result.errors.age).toBe('يجب أن يكون على الأقل 18');
});
});
describe('specific validation functions', () => {
it('should validate email', () => {
const result1 = validateEmail('');
expect(result1.isValid).toBe(false);
const result2 = validateEmail('test@example.com');
expect(result2.isValid).toBe(true);
});
it('should validate phone', () => {
const result1 = validatePhone('invalid');
expect(result1.isValid).toBe(false);
const result2 = validatePhone('+966501234567');
expect(result2.isValid).toBe(true);
});
it('should validate password', () => {
const result1 = validatePassword('123');
expect(result1.isValid).toBe(false);
const result2 = validatePassword('password123');
expect(result2.isValid).toBe(true);
});
it('should validate username', () => {
const result1 = validateUsername('ab');
expect(result1.isValid).toBe(false);
const result2 = validateUsername('user123');
expect(result2.isValid).toBe(true);
});
it('should validate plate number', () => {
const result1 = validatePlateNumber('');
expect(result1.isValid).toBe(false);
const result2 = validatePlateNumber('ABC-1234');
expect(result2.isValid).toBe(true);
});
it('should validate year', () => {
const result1 = validateYear(1800);
expect(result1.isValid).toBe(false);
const result2 = validateYear(2023);
expect(result2.isValid).toBe(true);
});
it('should validate currency', () => {
const result1 = validateCurrency(-10);
expect(result1.isValid).toBe(false);
const result2 = validateCurrency(100.50);
expect(result2.isValid).toBe(true);
});
});
describe('sanitization functions', () => {
it('should sanitize strings', () => {
expect(sanitizeString(' hello world ')).toBe('hello world');
expect(sanitizeString('\t\n test \n\t')).toBe('test');
});
it('should sanitize numbers', () => {
expect(sanitizeNumber('123.45')).toBe(123.45);
expect(sanitizeNumber('invalid')).toBe(null);
expect(sanitizeNumber(456)).toBe(456);
});
it('should sanitize integers', () => {
expect(sanitizeInteger('123')).toBe(123);
expect(sanitizeInteger('123.45')).toBe(123);
expect(sanitizeInteger('invalid')).toBe(null);
});
it('should sanitize form data', () => {
const data = {
name: ' John Doe ',
age: 25,
description: '\t\n Some text \n\t',
};
const sanitized = sanitizeFormData(data);
expect(sanitized.name).toBe('John Doe');
expect(sanitized.age).toBe(25);
expect(sanitized.description).toBe('Some text');
});
});
describe('patterns', () => {
it('should match email pattern', () => {
expect(PATTERNS.email.test('test@example.com')).toBe(true);
expect(PATTERNS.email.test('invalid-email')).toBe(false);
});
it('should match phone pattern', () => {
expect(PATTERNS.phone.test('+966501234567')).toBe(true);
expect(PATTERNS.phone.test('0501234567')).toBe(true);
expect(PATTERNS.phone.test('invalid-phone')).toBe(false);
});
it('should match username pattern', () => {
expect(PATTERNS.username.test('user123')).toBe(true);
expect(PATTERNS.username.test('user_name')).toBe(true);
expect(PATTERNS.username.test('user-name')).toBe(false);
expect(PATTERNS.username.test('user name')).toBe(false);
});
it('should match numeric patterns', () => {
expect(PATTERNS.numeric.test('12345')).toBe(true);
expect(PATTERNS.numeric.test('123abc')).toBe(false);
expect(PATTERNS.decimal.test('123.45')).toBe(true);
expect(PATTERNS.decimal.test('123')).toBe(true);
expect(PATTERNS.decimal.test('123.45.67')).toBe(false);
});
});
});

View File

@@ -0,0 +1,443 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
createVehicle,
updateVehicle,
deleteVehicle,
getVehicles,
getVehicleById,
getVehiclesForSelect,
searchVehicles,
getVehicleStats
} from '../vehicle-management.server';
import { prisma } from '../db.server';
// Mock Prisma
vi.mock('../db.server', () => ({
prisma: {
vehicle: {
create: vi.fn(),
findMany: vi.fn(),
findUnique: vi.fn(),
findFirst: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
count: vi.fn(),
},
customer: {
findUnique: vi.fn(),
},
},
}));
describe('Vehicle Management', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.resetAllMocks();
});
describe('createVehicle', () => {
const validVehicleData = {
plateNumber: 'ABC-123',
bodyType: 'سيدان',
manufacturer: 'تويوتا',
model: 'كامري',
year: 2020,
transmission: 'Automatic',
fuel: 'Gasoline',
useType: 'personal',
ownerId: 1,
};
it('should create a vehicle successfully', async () => {
const mockVehicle = { id: 1, ...validVehicleData };
const mockCustomer = { id: 1, name: 'أحمد محمد' };
(prisma.vehicle.findUnique as any).mockResolvedValue(null);
(prisma.customer.findUnique as any).mockResolvedValue(mockCustomer);
(prisma.vehicle.create as any).mockResolvedValue(mockVehicle);
const result = await createVehicle(validVehicleData);
expect(result.success).toBe(true);
expect(result.vehicle).toEqual(mockVehicle);
expect(prisma.vehicle.create).toHaveBeenCalledWith({
data: expect.objectContaining({
plateNumber: 'ABC-123',
manufacturer: 'تويوتا',
model: 'كامري',
ownerId: 1,
}),
});
});
it('should fail if plate number already exists', async () => {
const existingVehicle = { id: 2, plateNumber: 'ABC-123' };
(prisma.vehicle.findUnique as any).mockResolvedValue(existingVehicle);
const result = await createVehicle(validVehicleData);
expect(result.success).toBe(false);
expect(result.error).toBe('رقم اللوحة موجود بالفعل');
expect(prisma.vehicle.create).not.toHaveBeenCalled();
});
it('should fail if owner does not exist', async () => {
(prisma.vehicle.findUnique as any).mockResolvedValue(null);
(prisma.customer.findUnique as any).mockResolvedValue(null);
const result = await createVehicle(validVehicleData);
expect(result.success).toBe(false);
expect(result.error).toBe('المالك غير موجود');
expect(prisma.vehicle.create).not.toHaveBeenCalled();
});
it('should handle database errors', async () => {
(prisma.vehicle.findUnique as any).mockResolvedValue(null);
(prisma.customer.findUnique as any).mockResolvedValue({ id: 1 });
(prisma.vehicle.create as any).mockRejectedValue(new Error('Database error'));
const result = await createVehicle(validVehicleData);
expect(result.success).toBe(false);
expect(result.error).toBe('حدث خطأ أثناء إنشاء المركبة');
});
});
describe('updateVehicle', () => {
const updateData = {
plateNumber: 'XYZ-789',
manufacturer: 'هوندا',
model: 'أكورد',
};
it('should update a vehicle successfully', async () => {
const existingVehicle = { id: 1, plateNumber: 'ABC-123', ownerId: 1 };
const updatedVehicle = { ...existingVehicle, ...updateData };
(prisma.vehicle.findUnique as any).mockResolvedValue(existingVehicle);
(prisma.vehicle.findFirst as any).mockResolvedValue(null);
(prisma.vehicle.update as any).mockResolvedValue(updatedVehicle);
const result = await updateVehicle(1, updateData);
expect(result.success).toBe(true);
expect(result.vehicle).toEqual(updatedVehicle);
expect(prisma.vehicle.update).toHaveBeenCalledWith({
where: { id: 1 },
data: expect.objectContaining({
plateNumber: 'XYZ-789',
manufacturer: 'هوندا',
model: 'أكورد',
}),
});
});
it('should fail if vehicle does not exist', async () => {
(prisma.vehicle.findUnique as any).mockResolvedValue(null);
const result = await updateVehicle(999, updateData);
expect(result.success).toBe(false);
expect(result.error).toBe('المركبة غير موجودة');
expect(prisma.vehicle.update).not.toHaveBeenCalled();
});
it('should fail if new plate number conflicts', async () => {
const existingVehicle = { id: 1, plateNumber: 'ABC-123' };
const conflictVehicle = { id: 2, plateNumber: 'XYZ-789' };
(prisma.vehicle.findUnique as any).mockResolvedValue(existingVehicle);
(prisma.vehicle.findFirst as any).mockResolvedValue(conflictVehicle);
const result = await updateVehicle(1, updateData);
expect(result.success).toBe(false);
expect(result.error).toBe('رقم اللوحة موجود بالفعل');
expect(prisma.vehicle.update).not.toHaveBeenCalled();
});
});
describe('deleteVehicle', () => {
it('should delete a vehicle successfully', async () => {
const existingVehicle = {
id: 1,
plateNumber: 'ABC-123',
maintenanceVisits: []
};
(prisma.vehicle.findUnique as any).mockResolvedValue(existingVehicle);
(prisma.vehicle.delete as any).mockResolvedValue(existingVehicle);
const result = await deleteVehicle(1);
expect(result.success).toBe(true);
expect(prisma.vehicle.delete).toHaveBeenCalledWith({
where: { id: 1 },
});
});
it('should fail if vehicle does not exist', async () => {
(prisma.vehicle.findUnique as any).mockResolvedValue(null);
const result = await deleteVehicle(999);
expect(result.success).toBe(false);
expect(result.error).toBe('المركبة غير موجودة');
expect(prisma.vehicle.delete).not.toHaveBeenCalled();
});
it('should fail if vehicle has maintenance visits', async () => {
const existingVehicle = {
id: 1,
plateNumber: 'ABC-123',
maintenanceVisits: [{ id: 1 }, { id: 2 }]
};
(prisma.vehicle.findUnique as any).mockResolvedValue(existingVehicle);
const result = await deleteVehicle(1);
expect(result.success).toBe(false);
expect(result.error).toContain('لا يمكن حذف المركبة لأنها تحتوي على 2 زيارة صيانة');
expect(prisma.vehicle.delete).not.toHaveBeenCalled();
});
});
describe('getVehicles', () => {
it('should return vehicles with pagination', async () => {
const mockVehicles = [
{
id: 1,
plateNumber: 'ABC-123',
manufacturer: 'تويوتا',
owner: { id: 1, name: 'أحمد محمد' }
},
{
id: 2,
plateNumber: 'XYZ-789',
manufacturer: 'هوندا',
owner: { id: 2, name: 'محمد علي' }
}
];
(prisma.vehicle.findMany as any).mockResolvedValue(mockVehicles);
(prisma.vehicle.count as any).mockResolvedValue(2);
const result = await getVehicles('', 1, 10);
expect(result.vehicles).toEqual(mockVehicles);
expect(result.total).toBe(2);
expect(result.totalPages).toBe(1);
expect(prisma.vehicle.findMany).toHaveBeenCalledWith({
where: {},
include: {
owner: {
select: {
id: true,
name: true,
phone: true,
email: true,
},
},
},
orderBy: { createdDate: 'desc' },
skip: 0,
take: 10,
});
});
it('should filter by search query', async () => {
(prisma.vehicle.findMany as any).mockResolvedValue([]);
(prisma.vehicle.count as any).mockResolvedValue(0);
await getVehicles('تويوتا', 1, 10);
expect(prisma.vehicle.findMany).toHaveBeenCalledWith({
where: {
OR: [
{ plateNumber: { contains: 'تويوتا' } },
{ manufacturer: { contains: 'تويوتا' } },
{ model: { contains: 'تويوتا' } },
{ bodyType: { contains: 'تويوتا' } },
{ owner: { name: { contains: 'تويوتا' } } },
],
},
include: expect.any(Object),
orderBy: { createdDate: 'desc' },
skip: 0,
take: 10,
});
});
it('should filter by owner ID', async () => {
(prisma.vehicle.findMany as any).mockResolvedValue([]);
(prisma.vehicle.count as any).mockResolvedValue(0);
await getVehicles('', 1, 10, 5);
expect(prisma.vehicle.findMany).toHaveBeenCalledWith({
where: { ownerId: 5 },
include: expect.any(Object),
orderBy: { createdDate: 'desc' },
skip: 0,
take: 10,
});
});
});
describe('getVehicleById', () => {
it('should return vehicle with full relationships', async () => {
const mockVehicle = {
id: 1,
plateNumber: 'ABC-123',
owner: { id: 1, name: 'أحمد محمد' },
maintenanceVisits: [
{ id: 1, visitDate: new Date(), cost: 100 }
]
};
(prisma.vehicle.findUnique as any).mockResolvedValue(mockVehicle);
const result = await getVehicleById(1);
expect(result).toEqual(mockVehicle);
expect(prisma.vehicle.findUnique).toHaveBeenCalledWith({
where: { id: 1 },
include: {
owner: true,
maintenanceVisits: {
include: {
customer: {
select: {
id: true,
name: true,
phone: true,
},
},
},
orderBy: { visitDate: 'desc' },
take: 10,
},
},
});
});
it('should return null if vehicle not found', async () => {
(prisma.vehicle.findUnique as any).mockResolvedValue(null);
const result = await getVehicleById(999);
expect(result).toBeNull();
});
});
describe('getVehicleStats', () => {
it('should calculate vehicle statistics', async () => {
const mockVehicle = {
id: 1,
suggestedNextVisitDate: new Date('2024-06-01'),
maintenanceVisits: [
{ cost: 150, visitDate: new Date('2024-03-01') }, // Most recent first (desc order)
{ cost: 200, visitDate: new Date('2024-02-01') },
{ cost: 100, visitDate: new Date('2024-01-01') },
]
};
(prisma.vehicle.findUnique as any).mockResolvedValue(mockVehicle);
const result = await getVehicleStats(1);
expect(result).toEqual({
totalVisits: 3,
totalSpent: 450,
lastVisitDate: new Date('2024-03-01'),
nextSuggestedVisitDate: new Date('2024-06-01'),
averageVisitCost: 150,
});
});
it('should return null if vehicle not found', async () => {
(prisma.vehicle.findUnique as any).mockResolvedValue(null);
const result = await getVehicleStats(999);
expect(result).toBeNull();
});
it('should handle vehicle with no visits', async () => {
const mockVehicle = {
id: 1,
suggestedNextVisitDate: null,
maintenanceVisits: []
};
(prisma.vehicle.findUnique as any).mockResolvedValue(mockVehicle);
const result = await getVehicleStats(1);
expect(result).toEqual({
totalVisits: 0,
totalSpent: 0,
lastVisitDate: undefined,
nextSuggestedVisitDate: undefined,
averageVisitCost: 0,
});
});
});
describe('searchVehicles', () => {
it('should search vehicles by query', async () => {
const mockVehicles = [
{
id: 1,
plateNumber: 'ABC-123',
manufacturer: 'تويوتا',
model: 'كامري',
year: 2020,
owner: { id: 1, name: 'أحمد محمد' }
}
];
(prisma.vehicle.findMany as any).mockResolvedValue(mockVehicles);
const result = await searchVehicles('تويوتا');
expect(result).toEqual(mockVehicles);
expect(prisma.vehicle.findMany).toHaveBeenCalledWith({
where: {
OR: [
{ plateNumber: { contains: 'تويوتا' } },
{ manufacturer: { contains: 'تويوتا' } },
{ model: { contains: 'تويوتا' } },
],
},
select: {
id: true,
plateNumber: true,
manufacturer: true,
model: true,
year: true,
owner: {
select: {
id: true,
name: true,
},
},
},
orderBy: { plateNumber: 'asc' },
take: 10,
});
});
it('should return empty array for short queries', async () => {
const result = await searchVehicles('a');
expect(result).toEqual([]);
expect(prisma.vehicle.findMany).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,379 @@
import { describe, it, expect } from 'vitest';
import { validateVehicle } from '../validation';
describe('Vehicle Validation', () => {
describe('validateVehicle', () => {
const validVehicleData = {
plateNumber: 'ABC-123',
bodyType: 'سيدان',
manufacturer: 'تويوتا',
model: 'كامري',
year: 2020,
transmission: 'Automatic',
fuel: 'Gasoline',
useType: 'personal',
ownerId: 1,
};
it('should validate a complete valid vehicle', () => {
const result = validateVehicle(validVehicleData);
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
it('should validate a minimal valid vehicle', () => {
const minimalData = {
plateNumber: 'ABC-123',
bodyType: 'سيدان',
manufacturer: 'تويوتا',
model: 'كامري',
year: 2020,
transmission: 'Automatic',
fuel: 'Gasoline',
useType: 'personal',
ownerId: 1,
};
const result = validateVehicle(minimalData);
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
describe('plateNumber validation', () => {
it('should require plate number', () => {
const result = validateVehicle({ ...validVehicleData, plateNumber: '' });
expect(result.isValid).toBe(false);
expect(result.errors.plateNumber).toBe('رقم اللوحة مطلوب');
});
it('should require plate number to not be just whitespace', () => {
const result = validateVehicle({ ...validVehicleData, plateNumber: ' ' });
expect(result.isValid).toBe(false);
expect(result.errors.plateNumber).toBe('رقم اللوحة مطلوب');
});
});
describe('bodyType validation', () => {
it('should require body type', () => {
const result = validateVehicle({ ...validVehicleData, bodyType: '' });
expect(result.isValid).toBe(false);
expect(result.errors.bodyType).toBe('نوع الهيكل مطلوب');
});
it('should require body type to not be just whitespace', () => {
const result = validateVehicle({ ...validVehicleData, bodyType: ' ' });
expect(result.isValid).toBe(false);
expect(result.errors.bodyType).toBe('نوع الهيكل مطلوب');
});
});
describe('manufacturer validation', () => {
it('should require manufacturer', () => {
const result = validateVehicle({ ...validVehicleData, manufacturer: '' });
expect(result.isValid).toBe(false);
expect(result.errors.manufacturer).toBe('الشركة المصنعة مطلوبة');
});
it('should require manufacturer to not be just whitespace', () => {
const result = validateVehicle({ ...validVehicleData, manufacturer: ' ' });
expect(result.isValid).toBe(false);
expect(result.errors.manufacturer).toBe('الشركة المصنعة مطلوبة');
});
});
describe('model validation', () => {
it('should require model', () => {
const result = validateVehicle({ ...validVehicleData, model: '' });
expect(result.isValid).toBe(false);
expect(result.errors.model).toBe('الموديل مطلوب');
});
it('should require model to not be just whitespace', () => {
const result = validateVehicle({ ...validVehicleData, model: ' ' });
expect(result.isValid).toBe(false);
expect(result.errors.model).toBe('الموديل مطلوب');
});
});
describe('year validation', () => {
it('should not validate undefined year (partial validation)', () => {
const result = validateVehicle({ ...validVehicleData, year: undefined as any });
expect(result.isValid).toBe(true);
expect(result.errors.year).toBeUndefined();
});
it('should reject year below minimum', () => {
const result = validateVehicle({ ...validVehicleData, year: 1989 });
expect(result.isValid).toBe(false);
expect(result.errors.year).toContain('السنة يجب أن تكون بين 1990');
});
it('should reject year above maximum', () => {
const currentYear = new Date().getFullYear();
const result = validateVehicle({ ...validVehicleData, year: currentYear + 2 });
expect(result.isValid).toBe(false);
expect(result.errors.year).toContain('السنة يجب أن تكون بين');
});
it('should accept current year', () => {
const currentYear = new Date().getFullYear();
const result = validateVehicle({ ...validVehicleData, year: currentYear });
expect(result.isValid).toBe(true);
});
it('should accept next year', () => {
const nextYear = new Date().getFullYear() + 1;
const result = validateVehicle({ ...validVehicleData, year: nextYear });
expect(result.isValid).toBe(true);
});
});
describe('transmission validation', () => {
it('should require transmission', () => {
const result = validateVehicle({ ...validVehicleData, transmission: '' });
expect(result.isValid).toBe(false);
expect(result.errors.transmission).toBe('نوع ناقل الحركة غير صحيح');
});
it('should accept valid transmission types', () => {
const automaticResult = validateVehicle({ ...validVehicleData, transmission: 'Automatic' });
const manualResult = validateVehicle({ ...validVehicleData, transmission: 'Manual' });
expect(automaticResult.isValid).toBe(true);
expect(manualResult.isValid).toBe(true);
});
it('should reject invalid transmission types', () => {
const result = validateVehicle({ ...validVehicleData, transmission: 'CVT' });
expect(result.isValid).toBe(false);
expect(result.errors.transmission).toBe('نوع ناقل الحركة غير صحيح');
});
});
describe('fuel validation', () => {
it('should require fuel type', () => {
const result = validateVehicle({ ...validVehicleData, fuel: '' });
expect(result.isValid).toBe(false);
expect(result.errors.fuel).toBe('نوع الوقود غير صحيح');
});
it('should accept valid fuel types', () => {
const validFuels = ['Gasoline', 'Diesel', 'Hybrid', 'Mild Hybrid', 'Electric'];
validFuels.forEach(fuel => {
const result = validateVehicle({ ...validVehicleData, fuel });
expect(result.isValid).toBe(true);
});
});
it('should reject invalid fuel types', () => {
const result = validateVehicle({ ...validVehicleData, fuel: 'Nuclear' });
expect(result.isValid).toBe(false);
expect(result.errors.fuel).toBe('نوع الوقود غير صحيح');
});
});
describe('cylinders validation', () => {
it('should accept valid cylinder counts', () => {
const validCylinders = [1, 2, 3, 4, 6, 8, 10, 12];
validCylinders.forEach(cylinders => {
const result = validateVehicle({ ...validVehicleData, cylinders });
expect(result.isValid).toBe(true);
});
});
it('should reject cylinder count below minimum', () => {
const result = validateVehicle({ ...validVehicleData, cylinders: 0 });
expect(result.isValid).toBe(false);
expect(result.errors.cylinders).toContain('عدد الأسطوانات يجب أن يكون بين 1');
});
it('should reject cylinder count above maximum', () => {
const result = validateVehicle({ ...validVehicleData, cylinders: 16 });
expect(result.isValid).toBe(false);
expect(result.errors.cylinders).toContain('عدد الأسطوانات يجب أن يكون بين 1 و 12');
});
it('should accept null/undefined cylinders', () => {
const result1 = validateVehicle({ ...validVehicleData, cylinders: undefined });
const result2 = validateVehicle({ ...validVehicleData, cylinders: null as any });
expect(result1.isValid).toBe(true);
expect(result2.isValid).toBe(true);
});
});
describe('engineDisplacement validation', () => {
it('should accept valid engine displacements', () => {
const validDisplacements = [1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0];
validDisplacements.forEach(engineDisplacement => {
const result = validateVehicle({ ...validVehicleData, engineDisplacement });
expect(result.isValid).toBe(true);
});
});
it('should reject engine displacement at zero', () => {
const result = validateVehicle({ ...validVehicleData, engineDisplacement: 0 });
expect(result.isValid).toBe(false);
expect(result.errors.engineDisplacement).toContain('سعة المحرك يجب أن تكون بين 0.1');
});
it('should reject engine displacement above maximum', () => {
const result = validateVehicle({ ...validVehicleData, engineDisplacement: 15.0 });
expect(result.isValid).toBe(false);
expect(result.errors.engineDisplacement).toContain('سعة المحرك يجب أن تكون بين 0.1 و 10');
});
it('should accept null/undefined engine displacement', () => {
const result1 = validateVehicle({ ...validVehicleData, engineDisplacement: undefined });
const result2 = validateVehicle({ ...validVehicleData, engineDisplacement: null as any });
expect(result1.isValid).toBe(true);
expect(result2.isValid).toBe(true);
});
});
describe('useType validation', () => {
it('should require use type', () => {
const result = validateVehicle({ ...validVehicleData, useType: '' });
expect(result.isValid).toBe(false);
expect(result.errors.useType).toBe('نوع الاستخدام غير صحيح');
});
it('should accept valid use types', () => {
const validUseTypes = ['personal', 'taxi', 'apps', 'loading', 'travel'];
validUseTypes.forEach(useType => {
const result = validateVehicle({ ...validVehicleData, useType });
expect(result.isValid).toBe(true);
});
});
it('should reject invalid use types', () => {
const result = validateVehicle({ ...validVehicleData, useType: 'military' });
expect(result.isValid).toBe(false);
expect(result.errors.useType).toBe('نوع الاستخدام غير صحيح');
});
});
describe('ownerId validation', () => {
it('should not validate undefined owner ID (partial validation)', () => {
const result = validateVehicle({ ...validVehicleData, ownerId: undefined as any });
expect(result.isValid).toBe(true);
expect(result.errors.ownerId).toBeUndefined();
});
it('should reject zero owner ID', () => {
const result = validateVehicle({ ...validVehicleData, ownerId: 0 });
expect(result.isValid).toBe(false);
expect(result.errors.ownerId).toBe('مالك المركبة مطلوب');
});
it('should reject negative owner ID', () => {
const result = validateVehicle({ ...validVehicleData, ownerId: -1 });
expect(result.isValid).toBe(false);
expect(result.errors.ownerId).toBe('مالك المركبة مطلوب');
});
it('should accept positive owner ID', () => {
const result = validateVehicle({ ...validVehicleData, ownerId: 5 });
expect(result.isValid).toBe(true);
});
});
describe('multiple validation errors', () => {
it('should return all validation errors', () => {
const invalidData = {
plateNumber: '',
bodyType: '',
manufacturer: '',
model: '',
year: 1980,
transmission: 'Invalid',
fuel: 'Invalid',
cylinders: 20,
engineDisplacement: 15.0,
useType: 'Invalid',
ownerId: 0,
};
const result = validateVehicle(invalidData);
expect(result.isValid).toBe(false);
expect(Object.keys(result.errors)).toContain('plateNumber');
expect(Object.keys(result.errors)).toContain('bodyType');
expect(Object.keys(result.errors)).toContain('manufacturer');
expect(Object.keys(result.errors)).toContain('model');
expect(Object.keys(result.errors)).toContain('year');
expect(Object.keys(result.errors)).toContain('transmission');
expect(Object.keys(result.errors)).toContain('fuel');
expect(Object.keys(result.errors)).toContain('cylinders');
expect(Object.keys(result.errors)).toContain('engineDisplacement');
expect(Object.keys(result.errors)).toContain('useType');
expect(Object.keys(result.errors)).toContain('ownerId');
});
});
describe('partial validation', () => {
it('should validate only provided fields', () => {
const partialData = {
plateNumber: 'XYZ-789',
year: 2021,
};
const result = validateVehicle(partialData);
expect(result.isValid).toBe(true);
expect(Object.keys(result.errors)).toHaveLength(0);
});
it('should validate only provided fields with errors', () => {
const partialData = {
plateNumber: '',
year: 1980,
cylinders: 20,
};
const result = validateVehicle(partialData);
expect(result.isValid).toBe(false);
expect(Object.keys(result.errors)).toContain('plateNumber');
expect(Object.keys(result.errors)).toContain('year');
expect(Object.keys(result.errors)).toContain('cylinders');
expect(Object.keys(result.errors)).not.toContain('manufacturer');
expect(Object.keys(result.errors)).not.toContain('model');
});
});
});
});