Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,18 @@ import { setupSwagger } from "./config/swagger.config.js";

import distanceRoutes from "./modules/global/distances/routes/distance.routes.js";

import productImageRoutes from './modules/images/routes/product-image.routes.js';
import userImageRoutes from './modules/images/routes/user-image.routes.js';
import storeImageRoutes from './modules/images/routes/store-image.routes.js';
import adminUserRoutes from "./modules/admin/users/admin-users.routes.js";
// Rutas de imágenes
import {
productImageRoutes,
userImageRoutes,
storeImageRoutes,
} from './modules/images/routes/index.js';

// Rutas de administración
import {
adminUsersRoutes,
adminCategoryRoutes,
} from "./modules/admin/index.js";

const app = express();

Expand Down Expand Up @@ -75,7 +83,8 @@ app.use('/api/session', sessionRoutes);
app.use("/api/orders", orderRouter);

// Rutas de administración
app.use("/api/admin", adminUserRoutes);
app.use("/api/admin", adminUsersRoutes);
app.use("/api/admin/categories", adminCategoryRoutes);

// Rutas de distancias
app.use("/api/distances", distanceRoutes);
Expand Down
9 changes: 7 additions & 2 deletions src/config/swagger.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import swaggerJsdoc from "swagger-jsdoc";
import swaggerUi from "swagger-ui-express";
import {
commonSchemas,
imageSchemas,
// /commerce
addressSchemas,
storeSchemas,
Expand All @@ -10,7 +11,8 @@ import {
productSchemas,
// /session
sessionSchemas,
imageSchemas
// /admin
adminCategorySchemas,
} from "../docs/schemas/index.js";

const options = {
Expand Down Expand Up @@ -43,6 +45,7 @@ const options = {
{ name: "Users", description: "Gestión de usuarios" },
{ name: "Orders", description: "Gestión de pedidos" },
{ name: "Images", description: "Gestión de imágenes" },
{ name: "Admin", description: "Gestión de administración" },
],
components: {
securitySchemes: {
Expand All @@ -64,7 +67,9 @@ const options = {
// /session
...sessionSchemas,
// /images
...imageSchemas
...imageSchemas,
// /admin
...adminCategorySchemas,
}
},
security: [
Expand Down
13 changes: 13 additions & 0 deletions src/docs/schemas/admin/admin-category.schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const adminCategorySchemas = {
AdminCategoryResponse: {
type: "object",
properties: {
id: { type: "integer", example: 1 },
name: { type: "string", example: "Electrónica" },
status: { type: "boolean", example: true },
productCount: { type: "integer", example: 42 },
createdAt: { type: "string", format: "date-time" },
updatedAt: { type: "string", format: "date-time" }
}
}
};
3 changes: 2 additions & 1 deletion src/docs/schemas/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./common.schema.js";
export * from "./image.schema.js";

export * from "./commerce/address.schema.js";
export * from "./commerce/store.schema.js";
Expand All @@ -8,4 +9,4 @@ export * from "./commerce/product.schema.js";

export * from "./session/session.schema.js";

export * from "./image.schema.js";
export * from "./admin/admin-category.schema.js";
Empty file removed src/docs/schemas/product.schema.js
Empty file.
Empty file removed src/docs/schemas/review.schema.js
Empty file.
8 changes: 8 additions & 0 deletions src/lib/validators.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { ValidationError } from "./errors.js";

export const validateId = (value) => {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new ValidationError(`ID debe ser un entero mayor a 0`);
}
return parsed;
};

export const parsePositiveInteger = (value, fieldName) => {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
Expand Down
12 changes: 12 additions & 0 deletions src/modules/admin/categories/admin-category.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { validateId } from "../../../lib/validators.js";
import { getAdminProductCategoryService } from "./admin-category.service.js";

export const getAdminProductCategory = async (req, res, next) => {
try {
const id = validateId(req.params.id);
const result = await getAdminProductCategoryService(id);
return res.status(200).json(result);
} catch (error) {
next(error);
}
};
41 changes: 41 additions & 0 deletions src/modules/admin/categories/admin-category.routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Router } from "express";
import authenticate from "../../../config/jwt.config.js";
import { requireRole } from "../../../middlewares/auth.middleware.js";
import { getAdminProductCategory } from "./admin-category.controller.js";
import { ROLES } from "../../../utils/contants/roles.constant.js";

const router = Router();

/**
* @swagger
* /api/admin/categories/{id}:
* get:
* summary: Ver categoría de productos (Admin)
* tags: [Admin]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Categoría encontrada
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AdminCategoryResponse'
* 400:
* description: ID inválido
* 401:
* description: No autenticado
* 403:
* description: Sin permisos de administrador
* 404:
* description: Categoría no encontrada
Comment thread
coderabbitai[bot] marked this conversation as resolved.
*/
router.get("/:id", authenticate, requireRole(ROLES.ADMIN), getAdminProductCategory);

export { router as adminCategoryRoutes };
27 changes: 27 additions & 0 deletions src/modules/admin/categories/admin-category.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { prisma } from "../../../lib/prisma.js";
import { NotFoundError } from "../../../lib/errors.js";

export const getAdminProductCategoryService = async (id) => {
const category = await prisma.productCategories.findUnique({
where: { id_product_category: id },
select: {
id_product_category: true,
name: true,
status: true,
created_at: true,
updated_at: true,
_count: { select: { products: true } }
}
});

if (!category) throw new NotFoundError("Categoría no encontrada");

return {
id: category.id_product_category,
name: category.name,
status: category.status,
productCount: category._count.products,
createdAt: category.created_at,
updatedAt: category.updated_at
};
};
2 changes: 2 additions & 0 deletions src/modules/admin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./categories/admin-category.routes.js";
export * from "./users/admin-users.routes.js";
2 changes: 1 addition & 1 deletion src/modules/admin/users/admin-users.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ const router = Router();
// GET /api/admin/users?search=&role=&status=&page=&limit=
router.get("/users", authenticate, requireRole(ROLES.ADMIN), parsePagination, getUsers);

export default router;
export { router as adminUsersRoutes };
Empty file.
3 changes: 3 additions & 0 deletions src/modules/images/routes/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./product-image.routes.js";
export * from "./user-image.routes.js";
export * from "./store-image.routes.js";
2 changes: 1 addition & 1 deletion src/modules/images/routes/product-image.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ router.post('/:id/image', authenticate, requireRole(ROLES.ADMIN, ROLES.SELLER),
router.put('/:id/image', authenticate, requireRole(ROLES.ADMIN, ROLES.SELLER), upload.single('image'), updateProductImage)
router.delete('/:id/image', authenticate, requireRole(ROLES.ADMIN, ROLES.SELLER), deleteProductImage)

export default router
export { router as productImageRoutes }
2 changes: 1 addition & 1 deletion src/modules/images/routes/store-image.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ router.post('/:id/image', authenticate, requireRole(ROLES.ADMIN, ROLES.SELLER),
router.put('/:id/image', authenticate, requireRole(ROLES.ADMIN, ROLES.SELLER), upload.single('image'), updateStoreImage)
router.delete('/:id/image', authenticate, requireRole(ROLES.ADMIN, ROLES.SELLER), deleteStoreImage)

export default router
export { router as storeImageRoutes }
3 changes: 1 addition & 2 deletions src/modules/images/routes/user-image.routes.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Router } from 'express'
import { upload } from '../../../middlewares/upload.middleware.js'
import authenticate from '../../../config/jwt.config.js'
import { requireRole } from '../../../middlewares/auth.middleware.js'
import {
getUserImage,
uploadUserImage,
Expand All @@ -19,4 +18,4 @@ router.post('/:id/image', authenticate, upload.single('image'), uploadUserImage)
router.put('/:id/image', authenticate, upload.single('image'), updateUserImage)
router.delete('/:id/image', authenticate, deleteUserImage)

export default router
export { router as userImageRoutes }
143 changes: 143 additions & 0 deletions tests/unit/admin/admin-category.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { vi, describe, it, expect, beforeEach } from "vitest";
import request from "supertest";
import app from "../../../src/app.js";
import { prisma } from "../../../src/lib/prisma.js";

// ─── MOCKS ────────────────────────────────────────────────────────
vi.mock("../../../src/lib/prisma.js", () => ({
prisma: {
productCategories: { findUnique: vi.fn() }
}
}));

vi.mock("../../../src/config/jwt.config.js", () => ({
default: (req, res, next) => {
const role = req.headers["x-test-role"];

if (!role) {
return res.status(401).json({ errors: { auth: { message: "No autenticado" } } });
}

const users = {
admin: { id_user: 1, role: "ADMIN" },
seller: { id_user: 10, role: "SELLER" },
customer: { id_user: 2, role: "CUSTOMER" },
};

req.user = users[role] ?? null;

if (!req.user) {
return res.status(401).json({ errors: { auth: { message: "No autenticado" } } });
}

next();
}
}));

// ─── HELPERS ──────────────────────────────────────────────────────
const asRole = (req, role) => req.set("x-test-role", role);

const mockCategory = {
id_product_category: 1,
name: "Electrónica",
status: true,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
_count: { products: 5 }
};

// ─── GET /api/admin/categories/:id ────────────────────────────────
describe("GET /api/admin/categories/:id", () => {
beforeEach(() => vi.resetAllMocks());

it("devuelve 401 cuando no hay token", async () => {
const res = await request(app).get("/api/admin/categories/1");

expect(res.status).toBe(401);
});

it("devuelve 403 cuando el usuario no es ADMIN", async () => {
const res = await asRole(
request(app).get("/api/admin/categories/1"),
"seller"
);

expect(res.status).toBe(403);
});

it("devuelve 403 cuando el usuario es CUSTOMER", async () => {
const res = await asRole(
request(app).get("/api/admin/categories/1"),
"customer"
);

expect(res.status).toBe(403);
});

it("devuelve 400 cuando el id no es un entero válido", async () => {
const res = await asRole(
request(app).get("/api/admin/categories/abc"),
"admin"
);

expect(res.status).toBe(400);
});

it("devuelve 404 cuando la categoría no existe", async () => {
prisma.productCategories.findUnique.mockResolvedValue(null);

const res = await asRole(
request(app).get("/api/admin/categories/999"),
"admin"
);

expect(res.status).toBe(404);
});

it("devuelve 200 con la categoría y productCount", async () => {
prisma.productCategories.findUnique.mockResolvedValue(mockCategory);

const res = await asRole(
request(app).get("/api/admin/categories/1"),
"admin"
);

expect(res.status).toBe(200);
expect(res.body).toMatchObject({
id: 1,
name: "Electrónica",
status: true,
productCount: 5
});
});

it("devuelve la categoría aunque status sea false (admin ve todo)", async () => {
prisma.productCategories.findUnique.mockResolvedValue({
...mockCategory,
status: false
});

const res = await asRole(
request(app).get("/api/admin/categories/1"),
"admin"
);

expect(res.status).toBe(200);
expect(res.body.status).toBe(false);
});

it("retorna productCount en 0 cuando la categoría no tiene productos", async () => {
prisma.productCategories.findUnique.mockResolvedValue({
...mockCategory,
_count: { products: 0 }
});

const res = await asRole(
request(app).get("/api/admin/categories/1"),
"admin"
);

expect(res.status).toBe(200);
expect(res.body.productCount).toBe(0);
});
});
Loading