- Borradores por vendedor (guardar/continuar/eliminar), se limpia al registrar - Pre-llenado con ?cotizacion=<id> (cliente, contacto, proyecto, montos); botón 'Iniciar alta' en Mis cotizaciones del cotizador - Liga remota pública /client/firma/<token> (JWT scope firma-remota, 72h, rate limit, solo datos mínimos sin PLD): firma digital + subida de documentos que prende checkboxes en Monday Apartado - Tokens de liga no sirven como sesión y viceversa - Tests: api 228, alta 82, cotizador 82
181 lines
6.0 KiB
TypeScript
181 lines
6.0 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import type { Request, Response } from 'express'
|
|
import {
|
|
eliminarBorrador,
|
|
guardarBorrador,
|
|
listarBorradores,
|
|
obtenerBorrador,
|
|
} from './borradores.controller.js'
|
|
|
|
// ---------- Prisma mockeado (los tests no tocan la BD) ----------
|
|
|
|
const prismaMock = vi.hoisted(() => ({
|
|
borradorAlta: {
|
|
findMany: vi.fn(),
|
|
findUnique: vi.fn(),
|
|
create: vi.fn(),
|
|
update: vi.fn(),
|
|
delete: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
|
|
|
function reqFake(overrides: Partial<Request> = {}): Request {
|
|
return {
|
|
params: {},
|
|
body: {},
|
|
usuarioToken: { sub: 'u-1', username: 'asesor uno', rol: 'vendedor' },
|
|
...overrides,
|
|
} as Request
|
|
}
|
|
|
|
function resFake() {
|
|
const json = vi.fn()
|
|
const end = vi.fn()
|
|
const status = vi.fn().mockReturnValue({ json, end })
|
|
return { res: { json, status } as unknown as Response, status, json, end }
|
|
}
|
|
|
|
const borradorFake = (overrides: Record<string, unknown> = {}) => ({
|
|
id: 'b-1',
|
|
usuarioId: 'u-1',
|
|
nombre: 'Sin nombre',
|
|
datosJson: '{"datosGenerales":{}}',
|
|
paso: 0,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
...overrides,
|
|
})
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('listarBorradores', () => {
|
|
it('devuelve solo los del usuario del token y SIN datosJson', async () => {
|
|
prismaMock.borradorAlta.findMany.mockResolvedValue([{ id: 'b-1', nombre: 'X' }])
|
|
const { res, json } = resFake()
|
|
|
|
await listarBorradores(reqFake(), res)
|
|
|
|
const args = prismaMock.borradorAlta.findMany.mock.calls[0]![0]
|
|
expect(args.where).toEqual({ usuarioId: 'u-1' })
|
|
expect(args.select).not.toHaveProperty('datosJson')
|
|
expect(json).toHaveBeenCalledWith([{ id: 'b-1', nombre: 'X' }])
|
|
})
|
|
})
|
|
|
|
describe('guardarBorrador', () => {
|
|
it('crea un borrador nuevo (201) con etiqueta del nombre del cliente', async () => {
|
|
prismaMock.borradorAlta.create.mockResolvedValue(borradorFake())
|
|
const { res, status } = resFake()
|
|
const datos = { datosGenerales: { nombreCompleto: 'JUAN PEREZ' } }
|
|
|
|
await guardarBorrador(reqFake({ body: { paso: 2, datos } }), res)
|
|
|
|
const creado = prismaMock.borradorAlta.create.mock.calls[0]![0].data
|
|
expect(creado.usuarioId).toBe('u-1')
|
|
expect(creado.nombre).toBe('JUAN PEREZ')
|
|
expect(creado.paso).toBe(2)
|
|
expect(JSON.parse(creado.datosJson)).toEqual(datos)
|
|
expect(status).toHaveBeenCalledWith(201)
|
|
})
|
|
|
|
it('usa "Sin nombre" cuando el cliente aún no tiene nombre', async () => {
|
|
prismaMock.borradorAlta.create.mockResolvedValue(borradorFake())
|
|
const { res } = resFake()
|
|
|
|
await guardarBorrador(reqFake({ body: { paso: 0, datos: {} } }), res)
|
|
|
|
expect(prismaMock.borradorAlta.create.mock.calls[0]![0].data.nombre).toBe('Sin nombre')
|
|
})
|
|
|
|
it('con id actualiza el borrador propio (upsert)', async () => {
|
|
prismaMock.borradorAlta.findUnique.mockResolvedValue(borradorFake())
|
|
prismaMock.borradorAlta.update.mockResolvedValue(borradorFake({ paso: 5 }))
|
|
const { res, json } = resFake()
|
|
|
|
await guardarBorrador(reqFake({ body: { id: 'b-1', paso: 5, datos: {} } }), res)
|
|
|
|
expect(prismaMock.borradorAlta.update).toHaveBeenCalledWith({
|
|
where: { id: 'b-1' },
|
|
data: expect.objectContaining({ paso: 5 }),
|
|
})
|
|
expect(prismaMock.borradorAlta.create).not.toHaveBeenCalled()
|
|
expect(json).toHaveBeenCalled()
|
|
})
|
|
|
|
it('rechaza actualizar un borrador de otro usuario (403)', async () => {
|
|
prismaMock.borradorAlta.findUnique.mockResolvedValue(borradorFake({ usuarioId: 'otro' }))
|
|
|
|
await expect(
|
|
guardarBorrador(reqFake({ body: { id: 'b-1', paso: 1, datos: {} } }), resFake().res),
|
|
).rejects.toMatchObject({ status: 403 })
|
|
expect(prismaMock.borradorAlta.update).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('un admin sí puede actualizar el borrador de otro usuario', async () => {
|
|
prismaMock.borradorAlta.findUnique.mockResolvedValue(borradorFake({ usuarioId: 'otro' }))
|
|
prismaMock.borradorAlta.update.mockResolvedValue(borradorFake())
|
|
const req = reqFake({
|
|
body: { id: 'b-1', paso: 1, datos: {} },
|
|
usuarioToken: { sub: 'admin-1', username: 'admin', rol: 'admin' },
|
|
})
|
|
|
|
await guardarBorrador(req, resFake().res)
|
|
|
|
expect(prismaMock.borradorAlta.update).toHaveBeenCalled()
|
|
})
|
|
|
|
it('404 si el id a actualizar no existe', async () => {
|
|
prismaMock.borradorAlta.findUnique.mockResolvedValue(null)
|
|
|
|
await expect(
|
|
guardarBorrador(reqFake({ body: { id: 'b-x', paso: 1, datos: {} } }), resFake().res),
|
|
).rejects.toMatchObject({ status: 404 })
|
|
})
|
|
})
|
|
|
|
describe('obtenerBorrador', () => {
|
|
it('devuelve el borrador completo al dueño', async () => {
|
|
prismaMock.borradorAlta.findUnique.mockResolvedValue(borradorFake())
|
|
const { res, json } = resFake()
|
|
|
|
await obtenerBorrador(reqFake({ params: { id: 'b-1' } }), res)
|
|
|
|
expect(json).toHaveBeenCalledWith(expect.objectContaining({ id: 'b-1', datosJson: expect.any(String) }))
|
|
})
|
|
|
|
it('403 al abrir el borrador de otro usuario', async () => {
|
|
prismaMock.borradorAlta.findUnique.mockResolvedValue(borradorFake({ usuarioId: 'otro' }))
|
|
|
|
await expect(obtenerBorrador(reqFake({ params: { id: 'b-1' } }), resFake().res)).rejects.toMatchObject({
|
|
status: 403,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('eliminarBorrador', () => {
|
|
it('borra el borrador propio (204)', async () => {
|
|
prismaMock.borradorAlta.findUnique.mockResolvedValue(borradorFake())
|
|
prismaMock.borradorAlta.delete.mockResolvedValue(borradorFake())
|
|
const { res, status, end } = resFake()
|
|
|
|
await eliminarBorrador(reqFake({ params: { id: 'b-1' } }), res)
|
|
|
|
expect(prismaMock.borradorAlta.delete).toHaveBeenCalledWith({ where: { id: 'b-1' } })
|
|
expect(status).toHaveBeenCalledWith(204)
|
|
expect(end).toHaveBeenCalled()
|
|
})
|
|
|
|
it('no borra el borrador de otro usuario (403)', async () => {
|
|
prismaMock.borradorAlta.findUnique.mockResolvedValue(borradorFake({ usuarioId: 'otro' }))
|
|
|
|
await expect(eliminarBorrador(reqFake({ params: { id: 'b-1' } }), resFake().res)).rejects.toMatchObject({
|
|
status: 403,
|
|
})
|
|
expect(prismaMock.borradorAlta.delete).not.toHaveBeenCalled()
|
|
})
|
|
})
|