-
404
-
Página no encontrada
-
+
+
+
404
+
+ Página no encontrada
+
+
No pudimos encontrar la página que estás buscando.
-
diff --git a/src/routes/order-confirmation/order-confirmation.loader.test.ts b/src/routes/order-confirmation/order-confirmation.loader.test.ts
new file mode 100644
index 0000000..a8475b1
--- /dev/null
+++ b/src/routes/order-confirmation/order-confirmation.loader.test.ts
@@ -0,0 +1,27 @@
+import { describe, expect, it } from "vitest";
+
+import { loader } from ".";
+
+describe("OrderConfirmation loader", () => {
+ // Helper function to create loader arguments
+ const createLoaderArgs = (orderId: string) => ({
+ params: { orderId },
+ request: new Request(`http://localhost/order-confirmation/${orderId}`),
+ context: {},
+ });
+
+ it("should return orderId from params", async () => {
+ // Step 1: Setup - Create test data
+ const testOrderId = "testOrderId-123"; // Example order ID
+
+ // Step 2: Mock - Not needed as loader has no dependencies
+
+ // Step 3: Call service function
+ const result = await loader(createLoaderArgs(testOrderId));
+
+ // Step 4: Verify expected behavior
+ expect(result).toEqual({
+ orderId: testOrderId,
+ });
+ });
+});
diff --git a/src/routes/order-confirmation/order-confirmation.test.tsx b/src/routes/order-confirmation/order-confirmation.test.tsx
new file mode 100644
index 0000000..becd17c
--- /dev/null
+++ b/src/routes/order-confirmation/order-confirmation.test.tsx
@@ -0,0 +1,50 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import OrderConfirmation from ".";
+import type { Route } from "./+types";
+
+// Creates minimal test props for OrderConfirmation component
+const createTestProps = (orderId = "test-123"): Route.ComponentProps => ({
+ loaderData: { orderId },
+ params: vi.fn() as any,
+ matches: vi.fn() as any,
+});
+
+describe("OrderConfirmation", () => {
+ describe("Success Messages Display", () => {
+ it("should display all success messages correctly", () => {
+ // Step 1: Setup - Create test props
+ const props = createTestProps();
+ // Step 2: Mock
+ // Step 3: Call - Render component
+ render();
+ // Step 4: Verify - Check all success messages
+ const expectedMessages = [
+ "¡Muchas gracias por tu compra!",
+ "Tu orden está en camino",
+ "Llegaremos a la puerta de tu domicilio lo antes posible",
+ ];
+ expectedMessages.forEach((message) => {
+ expect(screen.queryByText(message)).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe("Order Tracking Information", () => {
+ it("should display correct tracking code section", () => {
+ // Step 1: Setup - Create test props with a specific order ID
+ const testOrderId = "order-456";
+ const props = createTestProps(testOrderId);
+ // Step 2: Mock
+ // Step 3: Call - Render component
+ render();
+ // Step 4: Verify - Check tracking code section
+ const trackingCodeLabel = screen.queryByText("Código de seguimiento");
+ expect(trackingCodeLabel).toBeInTheDocument();
+
+ const trackingCode = screen.queryByText(testOrderId);
+ expect(trackingCode).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/routes/product/product.loader.test.ts b/src/routes/product/product.loader.test.ts
new file mode 100644
index 0000000..794b99b
--- /dev/null
+++ b/src/routes/product/product.loader.test.ts
@@ -0,0 +1,51 @@
+import { describe, expect, it, vi } from "vitest";
+
+import { createTestProduct } from "@/lib/utils.tests";
+import * as productService from "@/services/product.service";
+
+import { loader } from ".";
+
+// Mock the product service
+vi.mock("@/services/product.service", () => ({
+ getProductById: vi.fn(), // mock function
+}));
+
+const mockGetProductById = vi.mocked(productService.getProductById);
+
+describe("Product loader", () => {
+ const createLoaderArgs = (id: string) => ({
+ params: { id },
+ request: new Request(`http://localhost/products/${id}`),
+ context: {},
+ });
+
+ it("returns a product when it exists", async () => {
+ const mockProduct = createTestProduct();
+
+ mockGetProductById.mockResolvedValue(mockProduct);
+
+ const result = await loader(createLoaderArgs("1"));
+
+ expect(result.product).toBeDefined();
+ expect(result.product).toEqual(mockProduct);
+ expect(mockGetProductById).toHaveBeenCalledWith(1);
+ });
+
+ it("returns empty object when product does not exist", async () => {
+ mockGetProductById.mockRejectedValue(new Error("Product not found"));
+
+ const result = await loader(createLoaderArgs("999"));
+
+ expect(result).toEqual({});
+ expect(mockGetProductById).toHaveBeenCalledWith(999);
+ });
+
+ it("handles invalid product id", async () => {
+ mockGetProductById.mockRejectedValue(new Error("Invalid ID"));
+
+ const result = await loader(createLoaderArgs("invalid"));
+
+ expect(result).toEqual({});
+ expect(mockGetProductById).toHaveBeenCalledWith(NaN);
+ });
+});
diff --git a/src/routes/product/product.test.tsx b/src/routes/product/product.test.tsx
new file mode 100644
index 0000000..3d0bbee
--- /dev/null
+++ b/src/routes/product/product.test.tsx
@@ -0,0 +1,162 @@
+import { render, screen } from "@testing-library/react";
+import { useNavigation } from "react-router";
+import { describe, expect, it, vi } from "vitest";
+
+import { createTestProduct } from "@/lib/utils.tests";
+import type { Product as ProductType } from "@/models/product.model";
+
+import Product from ".";
+
+import type { Route } from "./+types";
+
+// Helper function to create a test navigation object
+const createTestNavigation = (overrides = {}) => ({
+ state: "idle",
+ location: undefined,
+ formMethod: undefined,
+ formAction: undefined,
+ formEncType: undefined,
+ formData: undefined,
+ json: undefined,
+ text: undefined,
+ ...overrides,
+});
+
+// Mock de react-router
+vi.mock("react-router", () => ({
+ Form: vi.fn(({ children }) => ),
+ useNavigation: vi.fn(() => createTestNavigation()),
+ Link: vi.fn(({ children, ...props }) => {children}),
+}));
+
+const createTestProps = (
+ productData: Partial = {}
+): Route.ComponentProps => ({
+ loaderData: { product: createTestProduct(productData) },
+ params: vi.fn() as any,
+ matches: vi.fn() as any,
+});
+
+describe("Product Component", () => {
+ describe("Rendering with valid product data", () => {
+ it("should render product title correctly", () => {
+ // Step 1: Setup - Create test props
+ const props = createTestProps({ title: "Awesome Product" });
+ // Step 2: Mock - Component mocks already set up above
+ // Step 3: Call - Render component
+ render();
+ // Step 4: Verify - Check title is rendered correctly
+ const titleElement = screen.getByRole("heading", { level: 1 });
+ expect(titleElement).toHaveTextContent("Awesome Product");
+ });
+
+ it("should render product price with dollar sign", () => {
+ // Step 1: Setup - Create test props
+ const props = createTestProps({ price: 150.99 });
+ // Step 2: Mock - Component mocks already set up above
+ // Step 3: Call - Render component
+ render();
+ // Step 4: Verify - Check price is rendered correctly
+ expect(screen.queryByText("$150.99")).toBeInTheDocument();
+ });
+
+ it("should render product description", () => {
+ // Step 1: Setup - Create test props
+ const props = createTestProps({
+ description: "Amazing product",
+ });
+ // Step 2: Mock - Component mocks already set up above
+ // Step 3: Call - Render component
+ render();
+ // Step 4: Verify - Check description is rendered
+ expect(screen.queryByText("Amazing product")).toBeInTheDocument();
+ });
+
+ it("should render product image with correct src and alt attributes", () => {
+ // Step 1: Setup - Create test props
+ const props = createTestProps({
+ imgSrc: "/test-image.jpg",
+ alt: "Test Product",
+ });
+ // Step 2: Mock - Component mocks already set up above
+ // Step 3: Call - Render component
+ render();
+ // Step 4: Verify - Check image attributes
+ const image = screen.getByRole("img");
+ expect(image).toHaveAttribute("src", "/test-image.jpg");
+ expect(image).toHaveAttribute("alt", "Test Product");
+ });
+
+ it("should render all product features as list items", () => {
+ // Step 1: Setup - Create test props
+ const features = ["Feature 1", "Feature 2", "Feature 3"];
+ const props = createTestProps({ features });
+ // Step 2: Mock - Component mocks already set up above
+ // Step 3: Call - Render component
+ render();
+ // Step 4: Verify - Check features are rendered
+ features.forEach((feature) => {
+ expect(screen.queryByText(feature)).toBeInTheDocument();
+ });
+ });
+
+ it('should render "Agregar al Carrito" button', () => {
+ // Step 1: Setup - Create test props
+ const props = createTestProps();
+ // Step 2: Mock - Component mocks already set up above
+ // Step 3: Call - Render component
+ render();
+ // Step 4: Verify - Check button is present
+ expect(
+ screen.queryByRole("button", { name: "Agregar al Carrito" })
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("Form interactions", () => {
+ it("should include hidden redirectTo input with correct value", () => {
+ // Step 1: Setup
+ const productId = 123;
+ const props = createTestProps({ id: productId });
+ // Step 2: Mock - Component mocks already set up above
+ // Step 3: Call
+ render();
+ // Step 4: Verify
+ const redirectInput = screen.queryByDisplayValue(
+ `/products/${productId}`
+ );
+ expect(redirectInput).toBeInTheDocument();
+ });
+
+ it("should disable button when cart is loading", () => {
+ // Step 1: Setup
+ const props = createTestProps();
+ const expectedNavigation = createTestNavigation({ state: "submitting" });
+ // Step 2: Mock - Override navigation state to simulate loading
+ vi.mocked(useNavigation).mockReturnValue(expectedNavigation as any);
+ // Step 3: Call
+ render();
+ // Step 4: Verify
+ const button = screen.getByRole("button");
+ expect(button).toBeDisabled();
+ expect(button).toHaveTextContent("Agregando...");
+ });
+ });
+
+ describe("Error handling", () => {
+ it("should render NotFound component when product is not provided", () => {
+ // Step 1: Setup - Create props without product
+ const props = createTestProps();
+ props.loaderData.product = undefined;
+
+ // Step 2: Mock - Mock NotFound component
+ // vi.mock("../not-found", () => ({
+ // default: () => Not Found Page
,
+ // }));
+ // Step 3: Call
+ render();
+ // Step 4: Verify
+ expect(screen.getByTestId("not-found")).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/routes/root/components/auth-nav/auth-nav.test.tsx b/src/routes/root/components/auth-nav/auth-nav.test.tsx
new file mode 100644
index 0000000..09a5357
--- /dev/null
+++ b/src/routes/root/components/auth-nav/auth-nav.test.tsx
@@ -0,0 +1,69 @@
+import { render, screen } from "@testing-library/react";
+import { createMemoryRouter, RouterProvider } from "react-router";
+import { describe, it, expect } from "vitest";
+
+import type { User } from "@/models/user.model";
+import AuthNav from "@/routes/root/components/auth-nav";
+
+// Opción con Mock React Router components
+// vi.mock("react-router", () => ({
+// Form: ({ children, ...props }: any) => ,
+// Link: ({ children, to, ...props }: any) => (
+//
+// {children}
+//
+// ),
+// }));
+
+const renderWithRouter = (component: React.ReactElement) => {
+ const router = createMemoryRouter([
+ {
+ path: "/",
+ element: component,
+ },
+ ]);
+ return render();
+};
+
+describe("AuthNav Component", () => {
+ it("renders correctly when user doesn't exist", () => {
+ renderWithRouter();
+
+ expect(screen.queryByText("Iniciar sesión")).toBeInTheDocument();
+ expect(screen.queryByText("Crear una cuenta")).toBeInTheDocument();
+ });
+
+ it("renders correctly when user exists with name", () => {
+ const user: Omit = {
+ id: 1,
+ email: "testino@mail.com",
+ name: "Testino",
+ isGuest: false,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ renderWithRouter();
+
+ expect(screen.queryByText(`Bienvenido ${user.name}`)).toBeInTheDocument();
+ expect(screen.queryByText("Cerrar sesión")).toBeInTheDocument();
+ expect(screen.queryByText("Iniciar sesión")).not.toBeInTheDocument();
+ expect(screen.queryByText("Crear una cuenta")).not.toBeInTheDocument();
+ });
+
+ it("renders correctly when user exists with email only", () => {
+ const user: Omit = {
+ id: 1,
+ email: "testino@mail.com",
+ name: null,
+ isGuest: false,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ renderWithRouter();
+
+ expect(screen.queryByText(`Bienvenido ${user.email}`)).toBeInTheDocument();
+ expect(screen.queryByText("Cerrar sesión")).toBeInTheDocument();
+ });
+});
diff --git a/src/routes/signup/index.tsx b/src/routes/signup/index.tsx
index 05804dc..8c35e4b 100644
--- a/src/routes/signup/index.tsx
+++ b/src/routes/signup/index.tsx
@@ -87,7 +87,7 @@ export async function action({ request }: Route.ActionArgs) {
}
export async function loader({ request }: Route.LoaderArgs) {
- redirectIfAuthenticated(request);
+ await redirectIfAuthenticated(request);
}
export default function Signup({ actionData }: Route.ComponentProps) {
diff --git a/src/routes/signup/signup.loader.test.ts b/src/routes/signup/signup.loader.test.ts
new file mode 100644
index 0000000..5e5581d
--- /dev/null
+++ b/src/routes/signup/signup.loader.test.ts
@@ -0,0 +1,67 @@
+import { redirect } from "react-router";
+import { describe, expect, it, vi, afterEach } from "vitest";
+
+import { createTestRequest } from "@/lib/utils.tests";
+import * as AuthService from "@/services/auth.service";
+
+import { loader } from ".";
+
+import type { Route } from "./+types";
+
+vi.mock("@/services/auth.service", () => ({
+ redirectIfAuthenticated: vi.fn(),
+}));
+
+describe("signup.loader", () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("should call redirectIfAuthenticated with request", async () => {
+ // Step 1: Setup/Arrange
+ const request = createTestRequest();
+ const loaderArgs: Route.LoaderArgs = { request, params: {}, context: {} };
+
+ // Step 2: Mock
+ vi.mocked(AuthService.redirectIfAuthenticated).mockResolvedValueOnce(null);
+
+ // Step 3: Call/Act
+ await loader(loaderArgs);
+
+ // Step 4: Verify/Assert
+ expect(AuthService.redirectIfAuthenticated).toHaveBeenCalledTimes(1);
+ expect(AuthService.redirectIfAuthenticated).toHaveBeenCalledWith(request);
+ });
+
+ it("should return undefined when user is not authenticated", async () => {
+ // Step 1: Setup/Arrange
+ const request = createTestRequest();
+ const loaderArgs: Route.LoaderArgs = { request, params: {}, context: {} };
+
+ // Step 2: Mock
+ vi.mocked(AuthService.redirectIfAuthenticated).mockResolvedValueOnce(null);
+
+ // Step 3: Call/Act
+ const result = await loader(loaderArgs);
+
+ // Step 4: Verify/Assert
+ expect(result).toBeUndefined();
+ });
+
+ it("should throw redirect when user is authenticated", async () => {
+ // Step 1: Setup/Arrange
+ const request = createTestRequest();
+ const loaderArgs: Route.LoaderArgs = { request, params: {}, context: {} };
+ const redirectResponse = redirect("/");
+
+ // Step 2: Mock
+ vi.mocked(AuthService.redirectIfAuthenticated).mockImplementationOnce(
+ () => {
+ throw redirectResponse;
+ }
+ );
+
+ // Step 3 & 4: Call and Verify
+ await expect(loader(loaderArgs)).rejects.toBe(redirectResponse);
+ });
+});
diff --git a/src/services/category.service.test.ts b/src/services/category.service.test.ts
new file mode 100644
index 0000000..bb50a06
--- /dev/null
+++ b/src/services/category.service.test.ts
@@ -0,0 +1,77 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { createTestCategory } from "@/lib/utils.tests";
+import * as categoriesRepository from "@/repositories/category.repository";
+import {
+ getAllCategories,
+ getCategoryBySlug,
+} from "@/services/category.service";
+
+// Mock the repository
+vi.mock("@/repositories/category.repository");
+
+describe("Category Service", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("getAllCategories", () => {
+ it("should return all categories", async () => {
+ const mockCategories = [
+ createTestCategory(),
+ createTestCategory({
+ id: 2,
+ slug: "stickers",
+ title: "Stickers",
+ imgSrc: "/img/stickers.jpg",
+ alt: "Colección de stickers para programadores",
+ description:
+ "Explora nuestra colección de stickers para programadores",
+ }),
+ ];
+
+ vi.mocked(categoriesRepository.getAllCategories).mockResolvedValue(
+ mockCategories
+ );
+
+ const result = await getAllCategories();
+
+ expect(result).toEqual(mockCategories);
+ expect(categoriesRepository.getAllCategories).toHaveBeenCalledTimes(1);
+ });
+
+ it("should handle empty categories", async () => {
+ vi.mocked(categoriesRepository.getAllCategories).mockResolvedValue([]);
+
+ const result = await getAllCategories();
+
+ expect(result).toEqual([]);
+ expect(categoriesRepository.getAllCategories).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("getCategoryBySlug", () => {
+ it("should return category when found", async () => {
+ const mockCategory = createTestCategory();
+
+ vi.mocked(categoriesRepository.getCategoryBySlug).mockResolvedValue(
+ mockCategory
+ );
+
+ const result = await getCategoryBySlug("polos");
+
+ expect(result).toEqual(mockCategory);
+ expect(categoriesRepository.getCategoryBySlug).toHaveBeenCalledWith(
+ "polos"
+ );
+ });
+
+ it("should throw error when category not found", async () => {
+ vi.mocked(categoriesRepository.getCategoryBySlug).mockResolvedValue(null);
+
+ await expect(getCategoryBySlug("non-existent")).rejects.toThrow(
+ 'Category with slug "non-existent" not found'
+ );
+ });
+ });
+});
diff --git a/src/services/order.service.test.ts b/src/services/order.service.test.ts
new file mode 100644
index 0000000..28e99ff
--- /dev/null
+++ b/src/services/order.service.test.ts
@@ -0,0 +1,145 @@
+import { describe, expect, it, vi } from "vitest";
+
+import { calculateTotal } from "@/lib/cart";
+import {
+ createMockSession,
+ createTestRequest,
+ createTestUser,
+} from "@/lib/utils.tests";
+import type { CartItemInput } from "@/models/cart.model";
+import type { Order, OrderDetails } from "@/models/order.model";
+import * as orderRepository from "@/repositories/order.repository";
+import { getSession } from "@/session.server";
+
+import { createOrder, getOrdersByUser } from "./order.service";
+import { getOrCreateUser } from "./user.service";
+
+vi.mock("./user.service");
+vi.mock("@/lib/cart");
+vi.mock("@/repositories/order.repository");
+vi.mock("@/session.server");
+
+describe("Order Service", () => {
+ const mockedItems: CartItemInput[] = [
+ {
+ productId: 1,
+ quantity: 2,
+ title: "Test Product",
+ price: 19.99,
+ imgSrc: "test-product.jpg",
+ },
+ {
+ productId: 2,
+ quantity: 1,
+ title: "Another Product",
+ price: 29.99,
+ imgSrc: "another-product.jpg",
+ },
+ ];
+
+ const mockedFormData: OrderDetails = {
+ email: "test@test.com",
+ firstName: "",
+ lastName: "",
+ company: null,
+ address: "",
+ city: "",
+ country: "",
+ region: "",
+ zip: "",
+ phone: "",
+ };
+
+ const mockedUser = createTestUser();
+
+ const mockedOrder: Order = {
+ createdAt: "",
+ id: 1,
+ items: [
+ {
+ ...mockedItems[0],
+ id: 2,
+ orderId: 1,
+ createdAt: "",
+ updatedAt: "",
+ },
+ {
+ ...mockedItems[1],
+ id: 1,
+ orderId: 1,
+ createdAt: "",
+ updatedAt: "",
+ },
+ ],
+ totalAmount: 200,
+ userId: 1,
+ updatedAt: "",
+ details: mockedFormData,
+ };
+
+ const mockedTotalAmount = 200;
+
+ const mockedRequest = createTestRequest();
+
+ it("should create an order", async () => {
+ vi.mocked(getOrCreateUser).mockResolvedValue(mockedUser);
+ vi.mocked(calculateTotal).mockReturnValue(mockedTotalAmount);
+ vi.mocked(orderRepository.createOrderWithItems).mockResolvedValue(
+ mockedOrder
+ );
+
+ const order = await createOrder(mockedItems, mockedFormData);
+
+ expect(orderRepository.createOrderWithItems).toBeCalledWith(
+ mockedUser.id,
+ mockedItems,
+ mockedFormData,
+ mockedTotalAmount
+ );
+ expect(order).toEqual(mockedOrder);
+ });
+
+ it("should get orders by user", async () => {
+ const mockedOrders = [mockedOrder, { ...mockedOrder, id: 3 }];
+ const mockedSession = createMockSession(mockedUser.id); // Simulate updated user ID in session
+
+ vi.mocked(getSession).mockResolvedValue(mockedSession);
+ vi.mocked(orderRepository.getOrdersByUserId).mockResolvedValue(
+ mockedOrders
+ );
+
+ const orders = await getOrdersByUser(mockedRequest);
+
+ expect(orderRepository.getOrdersByUserId).toBeCalledWith(mockedUser.id);
+ expect(orders).toEqual(mockedOrders);
+ });
+
+ it("should throw error if user is not authenticated", async () => {
+ const mockedSession = createMockSession(null); // Simulate updated user ID in session
+
+ vi.mocked(getSession).mockResolvedValue(mockedSession);
+
+ await expect(getOrdersByUser(mockedRequest)).rejects.toThrow(
+ "User not authenticated"
+ );
+
+ expect(getSession).toHaveBeenCalledWith("session=mock-session-id");
+ });
+
+ it("should throw error if order is null", async () => {
+ vi.mocked(getOrCreateUser).mockResolvedValue(mockedUser);
+ vi.mocked(calculateTotal).mockReturnValue(mockedTotalAmount);
+ vi.mocked(orderRepository.createOrderWithItems).mockResolvedValue(null);
+
+ await expect(createOrder(mockedItems, mockedFormData)).rejects.toThrow(
+ "Failed to create order"
+ );
+
+ expect(orderRepository.createOrderWithItems).toBeCalledWith(
+ mockedUser.id,
+ mockedItems,
+ mockedFormData,
+ mockedTotalAmount
+ );
+ });
+});
diff --git a/src/services/order.service.ts b/src/services/order.service.ts
index 6285ef7..54e365a 100644
--- a/src/services/order.service.ts
+++ b/src/services/order.service.ts
@@ -8,9 +8,9 @@ import { getOrCreateUser } from "./user.service";
export async function createOrder(
items: CartItemInput[],
- formData: Record
+ formData: OrderDetails
): Promise {
- const shippingDetails = formData as unknown as OrderDetails;
+ const shippingDetails = formData;
const user = await getOrCreateUser(shippingDetails.email);
const totalAmount = calculateTotal(items);
diff --git a/src/services/product.service.test.ts b/src/services/product.service.test.ts
new file mode 100644
index 0000000..ea57513
--- /dev/null
+++ b/src/services/product.service.test.ts
@@ -0,0 +1,103 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { createTestCategory, createTestProduct } from "@/lib/utils.tests";
+import type { Category } from "@/models/category.model";
+import type { Product } from "@/models/product.model";
+import * as productRepository from "@/repositories/product.repository";
+
+import { getCategoryBySlug } from "./category.service";
+import { getProductById, getProductsByCategorySlug } from "./product.service";
+
+// Mock dependencies
+vi.mock("@/repositories/product.repository");
+vi.mock("./category.service");
+
+describe("Product Service", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("getProductsByCategorySlug", () => {
+ it("should return products for a valid category slug", async () => {
+ // Step 1: Setup - Create test data with valid category and products
+ const testCategory = createTestCategory();
+ const mockedProducts: Product[] = [
+ createTestProduct({ id: 1, categoryId: testCategory.id }),
+ createTestProduct({
+ id: 2,
+ title: "Test Product 2",
+ categoryId: testCategory.id,
+ }),
+ ];
+
+ // Step 2: Mock - Configure repository responses
+ vi.mocked(getCategoryBySlug).mockResolvedValue(testCategory);
+ vi.mocked(productRepository.getProductsByCategory).mockResolvedValue(
+ mockedProducts
+ );
+
+ // Step 3: Call service function
+ const products = await getProductsByCategorySlug(testCategory.slug);
+
+ // Step 4: Verify expected behavior
+ expect(getCategoryBySlug).toHaveBeenCalledWith(testCategory.slug);
+ expect(productRepository.getProductsByCategory).toHaveBeenCalledWith(
+ testCategory.id
+ );
+ expect(products).toEqual(mockedProducts);
+ });
+
+ it("should throw error when category slug does not exist", async () => {
+ // Step 1: Setup - Create test data for non-existent category
+ const invalidSlug = "invalid-slug";
+
+ // Step 2: Mock - Configure error response
+ const errorMessage = `Category with slug "${invalidSlug}" not found`;
+ vi.mocked(getCategoryBySlug).mockRejectedValue(new Error(errorMessage));
+
+ // Step 3: Call service function
+ const getProducts = getProductsByCategorySlug(
+ invalidSlug as Category["slug"]
+ );
+
+ // Step 4: Verify expected behavior
+ await expect(getProducts).rejects.toThrow(errorMessage);
+ expect(productRepository.getProductsByCategory).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("getProductById", () => {
+ it("should return product for valid ID", async () => {
+ // Step 1: Setup - Create test data for existing product
+ const testProduct = createTestProduct();
+
+ // Step 2: Mock - Configure repository response
+ vi.mocked(productRepository.getProductById).mockResolvedValue(
+ testProduct
+ );
+
+ // Step 3: Call service function
+ const result = await getProductById(testProduct.id);
+
+ // Step 4: Verify expected behavior
+ expect(productRepository.getProductById).toHaveBeenCalledWith(
+ testProduct.id
+ );
+ expect(result).toEqual(testProduct);
+ });
+
+ it("should throw error when product does not exist", async () => {
+ // Step 1: Setup - Configure ID for non-existent product
+ const nonExistentId = 999;
+
+ // Step 2: Mock - Configure null response from repository
+ vi.mocked(productRepository.getProductById).mockResolvedValue(null);
+
+ // Step 3: Call service function
+ const productPromise = getProductById(nonExistentId);
+
+ // Step 4: Verify expected behavior
+ await expect(productPromise).rejects.toThrow("Product not found");
+ });
+ });
+});
diff --git a/src/services/user.service.test.ts b/src/services/user.service.test.ts
new file mode 100644
index 0000000..ffe8965
--- /dev/null
+++ b/src/services/user.service.test.ts
@@ -0,0 +1,127 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { hashPassword } from "@/lib/security";
+import {
+ createMockSession,
+ createTestRequest,
+ createTestUser,
+} from "@/lib/utils.tests";
+import * as userRepository from "@/repositories/user.repository";
+import { getSession } from "@/session.server";
+
+import * as userService from "./user.service";
+
+// Mocking dependencies for unit tests
+vi.mock("@/session.server");
+vi.mock("@/repositories/user.repository");
+vi.mock("@/lib/security");
+
+describe("user service", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("updateUser", () => {
+ it("should update user details", async () => {
+ // Setup - Create mocks (test data)
+ const updatedUser = createTestUser();
+ const request = createTestRequest();
+ const mockSession = createMockSession(updatedUser.id); // Simulate updated user ID in session
+
+ // Mockeando las funciones que serán llamadas
+ vi.mocked(userRepository.updateUser).mockResolvedValue(updatedUser);
+ vi.mocked(getSession).mockResolvedValue(mockSession);
+
+ // Llamando al servicio y verificando el resultado
+ expect(await userService.updateUser(updatedUser, request)).toEqual(
+ updatedUser
+ );
+ });
+
+ it("should hash password if provided", async () => {
+ // Setup - Create mocks (test data)
+ const passwordBeforeHashing = "testing123";
+ const updatedUser = createTestUser({
+ id: 6,
+ password: passwordBeforeHashing,
+ });
+ const request = createTestRequest();
+ const mockSession = createMockSession(updatedUser.id); // Simulate updated user ID in session
+
+ // Mockeando las funciones que serán llamadas
+ vi.mocked(getSession).mockResolvedValue(mockSession);
+ vi.mocked(hashPassword).mockResolvedValue("hashed-password");
+
+ // Llamando al servicio y verificando el resultado
+ await userService.updateUser(updatedUser, request);
+
+ expect(hashPassword).toHaveBeenCalledWith(passwordBeforeHashing); // Verifica que se haya llamado a hashPassword con la contraseña original
+ expect(updatedUser.password).not.toBe(passwordBeforeHashing); // Verifica que la contraseña se haya actualizado
+ expect(updatedUser.password).toBe("hashed-password"); // Verifica que la contraseña se haya actualizado
+ });
+
+ it("should throw error if user is not authenticated", async () => {
+ // Setup - Create mocks (test data)
+ const updatedUser = createTestUser(); // No user ID provided
+ const request = createTestRequest();
+ const mockSession = createMockSession(null); // Simulate no user ID in session
+
+ // Mockeando las funciones que serán llamadas
+ vi.mocked(getSession).mockResolvedValue(mockSession);
+
+ // Llamando al servicio y verificando el resultado
+ await expect(
+ userService.updateUser(updatedUser, request)
+ ).rejects.toThrow("User not authenticated");
+
+ expect(getSession).toHaveBeenCalledWith("session=mock-session-id");
+ });
+ });
+
+ describe("getOrCreateUser", () => {
+ it("should return existing user when email is found", async () => {
+ // Setup - Create mock data
+ const email = "test@example.com";
+ const existingUser = createTestUser({
+ email,
+ id: 10,
+ });
+
+ // Mock repository function to return existing user
+ vi.mocked(userRepository.getUserByEmail).mockResolvedValue(existingUser);
+
+ // Call service function
+ const result = await userService.getOrCreateUser(email);
+
+ // Verify results
+ expect(result).toEqual(existingUser);
+ expect(userRepository.getUserByEmail).toHaveBeenCalledWith(email);
+ expect(userRepository.createUser).not.toHaveBeenCalled();
+ });
+
+ it("should create a new guest user when email is not found", async () => {
+ // Setup - Create mock data
+ const email = "test@example.com";
+ const newUser = createTestUser({
+ email,
+ id: 20,
+ isGuest: true,
+ });
+ const createUserDTO = {
+ email,
+ password: null,
+ isGuest: true,
+ name: null,
+ };
+ // Mock repository functions
+ vi.mocked(userRepository.getUserByEmail).mockResolvedValue(null);
+ vi.mocked(userRepository.createUser).mockResolvedValue(newUser);
+ // Call service function
+ const result = await userService.getOrCreateUser(email);
+ // Verify results
+ expect(result).toEqual(newUser);
+ expect(userRepository.getUserByEmail).toHaveBeenCalledWith(email);
+ expect(userRepository.createUser).toHaveBeenCalledWith(createUserDTO);
+ });
+ });
+});
diff --git a/testing-strategies.md b/testing-strategies.md
new file mode 100644
index 0000000..168f02c
--- /dev/null
+++ b/testing-strategies.md
@@ -0,0 +1,257 @@
+# Testing Strategies
+
+## 1. Component Testing Strategy
+
+### UI Components
+
+Test your reusable UI components in isolation:
+
+```typescript
+// Example: src/components/ui/button/button.test.tsx
+import { render, screen } from "@testing-library/react";
+import { describe, it, expect } from "vitest";
+import { Button } from "./button";
+
+describe("Button Component", () => {
+ it("renders correctly with different variants", () => {
+ render(Click me);
+ const button = screen.getByRole("button");
+ expect(button).toHaveClass("bg-secondary");
+ });
+});
+```
+
+### Route Components with Mock Router
+
+Create a test utility for rendering components with React Router context:
+
+```typescript
+// src/test-utils/router-utils.tsx
+import { createMemoryRouter, RouterProvider } from "react-router";
+import { render } from "@testing-library/react";
+
+export const renderWithRouter = (
+ component: React.ReactElement,
+ initialEntries = ["/"]
+) => {
+ const router = createMemoryRouter(
+ [
+ {
+ path: "*",
+ element: component,
+ },
+ ],
+ { initialEntries }
+ );
+
+ return render();
+};
+```
+
+## 2. Route Module Testing Strategy
+
+### Testing Loaders
+
+Test loaders independently from components:
+
+```typescript
+// src/routes/product/product.loader.test.ts
+import { describe, expect, it, vi } from "vitest";
+import { loader } from "./index";
+import * as productService from "@/services/product.service";
+
+vi.mock("@/services/product.service");
+
+describe("Product Loader", () => {
+ it("returns product data when product exists", async () => {
+ const mockProduct = { id: 1, title: "Test Product" };
+ vi.mocked(productService.getProductById).mockResolvedValue(mockProduct);
+
+ const result = await loader({
+ params: { id: "1" },
+ request: new Request("http://localhost/products/1"),
+ context: {},
+ });
+
+ expect(result.product).toEqual(mockProduct);
+ });
+});
+```
+
+### Testing Actions
+
+Test form actions and server-side logic:
+
+```typescript
+// src/routes/cart/add-item/add-item.action.test.ts
+import { describe, expect, it, vi } from "vitest";
+import { action } from "./index";
+import * as cartLib from "@/lib/cart";
+
+vi.mock("@/lib/cart");
+vi.mock("@/session.server");
+
+describe("Add Item Action", () => {
+ it("adds item to cart and redirects", async () => {
+ const formData = new FormData();
+ formData.append("productId", "1");
+ formData.append("quantity", "2");
+
+ const request = new Request("http://localhost", {
+ method: "POST",
+ body: formData,
+ });
+
+ const result = await action({ request, params: {}, context: {} });
+
+ expect(vi.mocked(cartLib.addToCart)).toHaveBeenCalledWith(
+ undefined, // userId
+ undefined, // sessionCartId
+ 1, // productId
+ 2 // quantity
+ );
+ });
+});
+```
+
+## 3. Integration Testing Strategy
+
+### Full Route Testing
+
+Test complete user flows with mocked services:
+
+```typescript
+// src/routes/product/product.integration.test.tsx
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { createMemoryRouter, RouterProvider } from "react-router";
+import { describe, it, expect, vi } from "vitest";
+
+import ProductRoute from "./index";
+import * as productService from "@/services/product.service";
+
+vi.mock("@/services/product.service");
+
+// Alternative to `renderWithRouter`
+const createTestRouter = (loaderData: any) => {
+ return createMemoryRouter(
+ [
+ {
+ path: "/products/:id",
+ element: ,
+ },
+ ],
+ { initialEntries: ["/products/1"] }
+ );
+};
+
+describe("Product Route Integration", () => {
+ it("displays product and allows adding to cart", async () => {
+ const mockProduct = {
+ id: 1,
+ title: "Test Product",
+ price: 99.99,
+ description: "Test description",
+ };
+
+ const router = createTestRouter({ product: mockProduct });
+ render();
+
+ expect(screen.getByText("Test Product")).toBeInTheDocument();
+
+ const addToCartButton = screen.getByText("Agregar al Carrito");
+ await userEvent.click(addToCartButton);
+
+ // Test form submission behavior
+ });
+});
+```
+
+## 4. Session Testing Strategy
+
+Create utilities for testing session-dependent functionality:
+
+```typescript
+// src/test-utils/session-utils.ts
+export const createMockSession = (data: Partial = {}) => ({
+ get: vi.fn((key: string) => data[key as keyof SessionData]),
+ set: vi.fn(),
+ unset: vi.fn(),
+ has: vi.fn((key: string) => key in data),
+});
+
+export const createMockRequest = (
+ url = "http://localhost",
+ options: RequestInit = {},
+ sessionData: Partial = {}
+) => {
+ const request = new Request(url, options);
+ // Mock session data somehow - you might need to adjust based on your session implementation
+ return request;
+};
+```
+
+## 5. Service Layer Testing
+
+Test your service functions independently:
+
+```typescript
+// src/services/cart.service.test.ts
+import { describe, expect, it, vi } from "vitest";
+import { addToCart } from "@/lib/cart";
+import * as cartRepository from "@/repositories/cart.repository";
+
+vi.mock("@/repositories/cart.repository");
+
+describe("Cart Service", () => {
+ it("creates new cart for guest user", async () => {
+ vi.mocked(cartRepository.getCart).mockResolvedValue(null);
+ vi.mocked(cartRepository.createCart).mockResolvedValue({
+ id: 1,
+ items: [],
+ sessionCartId: "test-session",
+ });
+
+ await addToCart(undefined, undefined, 1, 2);
+
+ expect(cartRepository.createCart).toHaveBeenCalled();
+ });
+});
+```
+
+## 6. Mock Strategy
+
+Create mock objects to use on your tests. You can also create functions that create those objects
+
+```typescript
+// src/test-utils/mocks.ts
+export const mockUser = {
+ id: 1,
+ email: "test@example.com",
+ name: "Test User",
+ isGuest: false,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+};
+
+export const mockCart = {
+ id: 1,
+ items: [],
+ sessionCartId: "test-session",
+ userId: 1,
+};
+
+export const mockProduct = {
+ id: 1,
+ title: "Test Product",
+ price: 99.99,
+ description: "Test description",
+ imgSrc: "/test.jpg",
+ features: ["Feature 1"],
+ alt: "Test alt",
+ categoryId: 1,
+ isOnSale: false,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+};
+```
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..44c2a13
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,16 @@
+import path from "node:path";
+
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: "jsdom",
+ setupFiles: ["./vitest.setup.ts"],
+ },
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+});
diff --git a/vitest.setup.ts b/vitest.setup.ts
new file mode 100644
index 0000000..d0de870
--- /dev/null
+++ b/vitest.setup.ts
@@ -0,0 +1 @@
+import "@testing-library/jest-dom";