feat(alta): borrador, pre-llenado desde cotización y liga remota de firma
- 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
This commit is contained in:
180
api-gateway/src/controllers/borradores.controller.test.ts
Normal file
180
api-gateway/src/controllers/borradores.controller.test.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user