Ticket — Casos de prueba de controladores, DTOs, jobs y repositorios
Documenta los specs de src/app/modules/ticket/:
controllers/ticket.controller.spec.ts, ticket.controller.filter-agents.spec.ts, ticket.controller.filter-permission.spec.ts y ticket.controller.permissions.spec.ts
dtos/transfer-ticket.dto.spec.ts
jobs/csat-expiration.job.spec.ts, jobs/ticket-reactivation.job.spec.ts y jobs/ticket-inactivity-resolution.job.spec.ts
repositories/ticket-contact-reason.repository.spec.ts y repositories/ticket.repository.spec.ts
Los specs de controladores son unitarios (o de guards con supertest); los de repositorios usan MongoMemoryServer con modelos reales; DTOs y jobs usan mocks.
Resumen: 10 specs, 44 suites (describe) y 106 tests (25 + 3 + 3 + 4 en controladores, 9 en DTO, 3 + 2 + 9 en jobs, 4 + 44 en repositorios).
controllers/ticket.controller.spec.ts
Suite TicketController. Tests unitarios del controlador: se invocan los métodos directamente (los guards AuthGuard y PermissionsGuard se sobrescriben con canActivate: () => true). El tenant se obtiene de user.tenantId.toString().
Mocks: TicketService (getTicketsList, postponeTicket, cancelPostpone, markUnread, resolveTicket, deleteTicket, lockTicket, unlockTicket, forceUnlockTicket), TranslateService.get (devuelve la clave), SocketBroadcastService (broadcastTicketStatusChanged, broadcastTicketDeleted, broadcastTicketLockChanged), PermissionResolverService y UserService.
getTicketsList
| Test | Escenario | Resultado esperado | Mocks |
|---|
should call service with tenantId from current user | Llamada con filtros vacíos. | getTicketsList(MOCK_TENANT_ID, {}). | TicketService |
should return translated message, results and meta | El servicio devuelve un ticket y meta. | { message: 'ticket.findAll', data: { results, meta } }. | TicketService, TranslateService |
should forward filters to the service | Filtros status: 'open' y hasUnread: true. | El servicio recibe esos filtros intactos. | TicketService |
should return empty results when service returns empty array | Servicio devuelve resultados vacíos. | data.results vacío y meta.total 0. | TicketService |
should use tenantId.toString() so it works with ObjectId or string tenantId | Usuario con tenantId como ObjectId. | El servicio recibe el tenant como string. | TicketService |
should call translate with FIND_ALL key and lang | Llamada con idioma es. | translate.get('ticket.findAll', 'es'). | TranslateService |
postponeTicket
| Test | Escenario | Resultado esperado | Mocks |
|---|
forwards ticketId, date, tenantId and requesting user to the service | Body con postponedUntil en ISO. | postponeTicket(ticketId, Date, tenantId, userId, undefined, expect.any(String)). | TicketService |
broadcasts the postponed status change and returns the updated ticket | El servicio devuelve el ticket aplazado. | broadcastTicketStatusChanged(tenant, ticketId, 'postponed') y respuesta ticket.updated. | TicketService, broadcastTicketStatusChanged |
cancelPostpone
| Test | Escenario | Resultado esperado | Mocks |
|---|
forwards ticketId, tenantId and requesting user to the service | Cancelación del aplazamiento. | cancelPostpone(ticketId, tenantId, userId, undefined, expect.any(String)). | TicketService |
broadcasts the open status change and returns the updated ticket | El servicio devuelve el ticket open. | broadcastTicketStatusChanged(tenant, ticketId, 'open') y respuesta ticket.updated. | TicketService, broadcastTicketStatusChanged |
resolveTicket vs resolveNoFarewellTicket (regression parity)
| Test | Escenario | Resultado esperado | Mocks |
|---|
both routes broadcast the same "resolved" status-change effect and return the same response shape | Se llama a resolveTicket y luego a resolveNoFarewellTicket. | Mismos argumentos de broadcast (tenant, ticketId, 'resolved') y misma respuesta ticket.updated. | TicketService.resolveTicket, broadcastTicketStatusChanged |
both routes invoke the resolve service with the same ticket and tenant identifiers | Mismas dos llamadas. | resolveTicket recibe el mismo ticketId y tenantId en ambas rutas. | TicketService.resolveTicket |
markUnread
| Test | Escenario | Resultado esperado | Mocks |
|---|
forwards ticketId, agentUnread and tenantId to the service | Body agentUnread: true. | markUnread(ticketId, true, tenantId). | TicketService |
returns the translated message and the updated ticket | Body agentUnread: false. | { message: 'ticket.updated', data: ticket }. | TicketService, TranslateService |
deleteTicket
| Test | Escenario | Resultado esperado | Mocks |
|---|
forwards ticketId and tenantId to the service | Eliminación de un ticket. | deleteTicket(ticketId, tenantId). | TicketService |
broadcasts ticket_deleted to the owning tenant room only after the service call resolves | Se registra el orden de las llamadas. | Orden service y luego broadcast; broadcastTicketDeleted(tenantId, ticketId). | deleteTicket, broadcastTicketDeleted |
does not broadcast when the service call throws (e.g. ticket not found) | El servicio rechaza con nope. | Propaga el error y no hay broadcast. | deleteTicket rechaza |
returns the translated deleted message | Eliminación exitosa. | Devuelve { message: 'ticket.deleted' }. | TranslateService |
never fans out to a tenant other than the deleted ticket’s own tenant | Ticket del tenant A eliminado. | Un solo broadcast con el tenant A; nunca con el tenant B. | broadcastTicketDeleted |
lockTicket
| Test | Escenario | Resultado esperado | Mocks |
|---|
forwards ticketId, tenantId and requesting user to the service | Bloqueo de un ticket. | lockTicket(ticketId, tenantId, userId). | TicketService |
broadcasts ticket_lock_changed with the requesting user as the new holder and returns the ticket | Bloqueo exitoso. | broadcastTicketLockChanged(tenant, { ticketId, lockedBy: userId, lockedByName: 'byron' }) y respuesta ticket.updated. | broadcastTicketLockChanged |
unlockTicket
| Test | Escenario | Resultado esperado | Mocks |
|---|
forwards ticketId, tenantId and requesting user to the service | Desbloqueo. | unlockTicket(ticketId, tenantId, userId). | TicketService |
broadcasts ticket_lock_changed with a null holder and returns the ticket | Desbloqueo exitoso. | Broadcast con lockedBy: null y lockedByName: null; respuesta ticket.updated. | broadcastTicketLockChanged |
forceUnlockTicket
| Test | Escenario | Resultado esperado | Mocks |
|---|
forwards ticketId and tenantId to the service (no ownership check at this layer) | Desbloqueo forzado. | forceUnlockTicket(ticketId, tenantId) sin usuario. | TicketService |
broadcasts ticket_lock_changed with a null holder and returns the ticket | Desbloqueo forzado exitoso. | Broadcast con titular null y respuesta ticket.updated. | broadcastTicketLockChanged |
controllers/ticket.controller.filter-agents.spec.ts
Suite TicketController — GET /tickets/filter-agents (D11). Endpoint dedicado GET /v1/tickets/filter-agents, protegido solo por el permiso TICKET_FILTER_BY_AGENT (sin depender de USER_READ). Usa el pipeline HTTP real (supertest, ValidationPipe, PermissionsGuard real); AuthGuard se reemplaza por un FakeAuthGuard que inyecta el usuario.
Mocks: PermissionResolverService.resolve (devuelve el set grantedAcronyms), UserService.findFilterableAgents (2 agentes), TicketService y SocketBroadcastService vacíos.
TicketController — GET /tickets/filter-agents (D11)
| Test | Escenario | Resultado esperado | Mocks |
|---|
returns 403 without TICKET_FILTER_BY_AGENT, even with TICKET_READ and USER_READ granted | Solo TICKET_READ y USER_READ. | HTTP 403 y no se llama a findFilterableAgents. | PermissionResolverService |
returns 200 with ONLY TICKET_FILTER_BY_AGENT granted — no USER_READ needed | Solo TICKET_FILTER_BY_AGENT. | HTTP 200; findFilterableAgents(tenantId) y 2 resultados en data.results. | PermissionResolverService, UserService |
response payload exposes only _id, firstname, lastName, username — no sensitive fields | Respuesta 200. | Cada agente tiene solo _id, firstname, lastName, username; sin email, password ni roleId. | UserService |
controllers/ticket.controller.filter-permission.spec.ts
Suite TicketController — assignedUserIds authorization guard-clause (D5). Prueba el rechazo dentro del controlador de assignedUserIds mediante PermissionResolverService (no un segundo @RequirePermission). Pipeline HTTP real con ValidationPipe({ transform: true }) para probar también la transformación toStringArray del DTO.
Mocks: TicketService.getTicketsList (resultado vacío), PermissionResolverService.resolve, UserService (solo para DI).
TicketController — assignedUserIds authorization guard-clause (D5)
| Test | Escenario | Resultado esperado | Mocks |
|---|
returns 200 and forwards no assignedUserIds when the param is absent, even without TICKET_FILTER_BY_AGENT (zero-cost hot path) | GET /v1/tickets/get-all sin assignedUserIds, solo con TICKET_READ. | HTTP 200 y filters.assignedUserIds es undefined. | getTicketsList |
returns 403 when assignedUserIds is sent without TICKET_FILTER_BY_AGENT (TICKET_READ alone is not enough) | Se envían dos assignedUserIds sin el permiso. | HTTP 403 y no se llama a getTicketsList. | PermissionResolverService |
returns 200 and forwards the normalized assignedUserIds array when TICKET_FILTER_BY_AGENT is granted | Se envían dos assignedUserIds con TICKET_FILTER_BY_AGENT. | HTTP 200 y filters.assignedUserIds es el arreglo normalizado de los dos ids. | getTicketsList |
controllers/ticket.controller.permissions.spec.ts
Prueba de integración de guards: el PermissionsGuard real y Reflector a través del pipeline HTTP (FakeAuthGuard inyecta tenant y rol; el resolver está mockeado), para demostrar que el guard está cableado y no solo que el método funciona.
Mocks: TicketService (resolveTicket, forceUnlockTicket), SocketBroadcastService, PermissionResolverService.resolve, UserService.
TicketController — coarse-update (TICKET_UPDATE) permission guard integration
| Test | Escenario | Resultado esperado | Mocks |
|---|
returns 403 Forbidden for a lifecycle-transition endpoint when TICKET_UPDATE is missing | PATCH /v1/tickets/resolve/:id con solo TICKET_READ. | HTTP 403 y no se llama a resolveTicket. | PermissionResolverService |
returns 200 OK for a lifecycle-transition endpoint when TICKET_UPDATE is granted | Mismo endpoint con TICKET_UPDATE. | HTTP 200 y se llama a resolveTicket. | PermissionResolverService, resolveTicket |
TicketController — force-unlock (TICKET_UNLOCK_ANY) permission guard integration
| Test | Escenario | Resultado esperado | Mocks |
|---|
returns 403 Forbidden when only TICKET_UPDATE is granted (NOT a role bypass — dedicated acronym required) | PATCH /v1/tickets/force-unlock/:id con solo TICKET_UPDATE. | HTTP 403 y no se llama a forceUnlockTicket. | PermissionResolverService |
returns 200 OK when TICKET_UNLOCK_ANY is granted | Mismo endpoint con TICKET_UNLOCK_ANY. | HTTP 200 y se llama a forceUnlockTicket. | PermissionResolverService, forceUnlockTicket |
dtos/transfer-ticket.dto.spec.ts
Suite TransferTicketDto — validation. Valida con class-validator (plainToInstance + validate) los campos del DTO de transferencia. userId es opcional: transferir solo a un departamento es válido; el servicio exige que haya al menos uno de userId o departmentId (400). No usa mocks.
userId
| Test | Escenario | Resultado esperado | Mocks |
|---|
passes when userId is a valid MongoId | userId con un ObjectId válido. | Sin errores para userId. | ninguno |
fails when userId is empty string | userId: ''. | Hay errores para userId. | ninguno |
passes when userId is missing but departmentId is present | Solo departmentId. | Sin errores para userId (campo opcional, SPEC 06). | ninguno |
fails when userId is not a valid MongoId | userId: 'not-a-mongo-id'. | Hay errores para userId. | ninguno |
departmentId
| Test | Escenario | Resultado esperado | Mocks |
|---|
passes when departmentId is absent (optional field) | Solo userId. | Sin errores para departmentId. | ninguno |
passes when departmentId is a valid MongoId | userId y departmentId válidos. | Sin errores para departmentId. | ninguno |
fails when departmentId is present but not a valid MongoId | departmentId: 'bad-id'. | Hay errores para departmentId. | ninguno |
internalNote
| Test | Escenario | Resultado esperado | Mocks |
|---|
passes when absent | Sin internalNote. | Sin errores para internalNote. | ninguno |
passes when present as string | internalNote: 'nota interna'. | Sin errores para internalNote. | ninguno |
jobs/csat-expiration.job.spec.ts
Suite CsatExpirationJob. Prueba el tick handleInterval que expira las encuestas CSAT vencidas. Mock: TicketRepository.expireCsatSurveys.
CsatExpirationJob
| Test | Escenario | Resultado esperado | Mocks |
|---|
expires due CSAT surveys via the repository on each tick | expireCsatSurveys devuelve 3. | Se llama a expireCsatSurveys con una Date. | TicketRepository |
does not throw and does not trigger any other ticket effect when nothing to expire | expireCsatSurveys devuelve 0. | handleInterval resuelve sin lanzar. | TicketRepository |
swallows errors so one bad tick does not stop future ticks | expireCsatSurveys rechaza (db down). | handleInterval resuelve sin lanzar. | TicketRepository rechaza |
jobs/ticket-reactivation.job.spec.ts
Suite TicketReactivationJob. Mock: TicketService.reactivateDueTickets.
TicketReactivationJob
| Test | Escenario | Resultado esperado | Mocks |
|---|
delegates to TicketService.reactivateDueTickets on every tick | handleInterval con reactivateDueTickets devolviendo []. | reactivateDueTickets se llama exactamente una vez. | TicketService |
swallows errors from reactivateDueTickets so one bad tick does not stop future ticks | reactivateDueTickets rechaza (db down). | handleInterval resuelve a undefined. | TicketService rechaza |
jobs/ticket-inactivity-resolution.job.spec.ts
Suite TicketInactivityResolutionJob. Prueba el tick que notifica y resuelve tickets por inactividad del contacto según la configuración efectiva (tenant o departamento). La configuración base tiene resolveInMinutes: 5, notifyInMinutes: 1 y un mensaje de aviso.
Mocks: TicketRepository (findOpenWithContactActivity, markAutoResolutionNotified), TicketResolutionEffectsService.apply, DepartmentRepository.findById, TenantSettingsService.getOrCreate, AutoResolutionPrecedenceService.resolve, ContactRepository, MessageTemplateService.interpolate, Message model create, OutboundRouterService.route y SocketBroadcastService.
TicketInactivityResolutionJob
| Test | Escenario | Resultado esperado | Mocks |
|---|
resolves a ticket via TicketResolutionEffectsService once resolveInMinutes is exceeded | Último mensaje del contacto hace 10 min, aviso ya enviado y resolveInMinutes: 5. | apply(ticket, { source: 'inactivity', useConnectionFarewell: true }) y no se vuelve a marcar el aviso. | findOpenWithContactActivity, precedence.resolve, apply |
sends the notification once notifyInMinutes is reached, without resolving yet | Último mensaje hace 2 min (entre notifyInMinutes y resolveInMinutes). | No resuelve; crea el mensaje de aviso, lo enruta y llama markAutoResolutionNotified(ticket._id). | Message.create, route, markAutoResolutionNotified |
notifies before resolving when the notify window was skipped | Hace 10 min pero sin aviso previo. | No resuelve todavía; marca el aviso como enviado. | apply, markAutoResolutionNotified |
resolves without notifying when notifyInMinutes is not below resolveInMinutes | notifyInMinutes igual a resolveInMinutes (5) y hace 10 min. | Resuelve con apply(...) y no crea mensaje de aviso. | apply, Message.create |
does not resend the notification when autoResolutionNotifiedAt is already set | Hace 2 min con autoResolutionNotifiedAt informado. | No crea mensaje ni vuelve a marcar el aviso. | Message.create, markAutoResolutionNotified |
does nothing for a ticket whose effective config has enabled=false | Configuración efectiva con enabled: false y hace 60 min. | No resuelve ni crea mensajes. | precedence.resolve, apply |
resolves using the department config when overrideGlobalResolution=true (precedence) | Ticket con departamento con overrideGlobalResolution: true y resolveInMinutes: 2; hace 3 min. | Consulta el departamento con findById(departmentId, tenantId), llama a precedence.resolve(department, config global) y resuelve con useConnectionFarewell: false. | DepartmentRepository, precedence.resolve, apply |
does not look up a department when the ticket has no departmentId | Ticket sin departmentId. | No se llama a DepartmentRepository.findById. | DepartmentRepository |
swallows errors so one bad tick does not stop future ticks | findOpenWithContactActivity rechaza (db down). | handleInterval resuelve a undefined. | TicketRepository rechaza |
Suite TicketContactReasonRepository — integration (mongodb-memory-server). Usa MongoDB en memoria con el modelo real; sin mocks. Los registros se limpian tras cada test.
create
| Test | Escenario | Resultado esperado | Mocks |
|---|
stores record and returns it with createdAt | Crea un registro con tenant, ticket, departamento y descripción. | Devuelve _id, ticketId, tenantId, departmentId, description y createdAt. | ninguno |
creates record without departmentId (undefined allowed) | Crea sin departmentId. | Se crea y devuelve _id. | ninguno |
findByTicket
| Test | Escenario | Resultado esperado | Mocks |
|---|
returns records for the given ticket and tenant, sorted newest first | Dos registros del ticket 1 (tenant A), uno de otro ticket y uno de otro tenant. | Devuelve solo 2 registros, todos del ticket y tenant solicitados. | ninguno |
returns empty array when no records exist for the ticket | Sin registros. | Devuelve []. | ninguno |
repositories/ticket.repository.spec.ts
Suite TicketRepository — integration (mongodb-memory-server). Tests de integración del repositorio con MongoDB en memoria; sin mocks de la capa de datos. Cubre aislamiento por tenant (REQ-13), aplazamiento, CSAT, bloqueo (ticket-lock) y operaciones atómicas con CAS (compare-and-set).
findByIdAndTenant
| Test | Escenario | Resultado esperado | Mocks |
|---|
REQ-13 Scenario 13.1 — returns null when tenantId does not match (cross-tenant) | Se busca el ticket del tenant A con el tenant B. | Devuelve null. | ninguno |
REQ-13 Scenario 13.2 — returns the full TicketDocument when _id and tenantId match | Id y tenant correctos. | Devuelve el documento con _id, tenantId y ContactId correctos. | ninguno |
returns null when _id does not exist in the collection at all | Id inexistente. | Devuelve null. | ninguno |
updateLastMessage
| Test | Escenario | Resultado esperado | Mocks |
|---|
sets lastContactMessageAt when isFromContact=true | Mensaje del contacto. | Guarda lastMessage y lastContactMessageAt como Date. | ninguno |
does NOT set lastContactMessageAt when isFromContact=false (agent message) | Mensaje de agente. | Guarda lastMessage; lastContactMessageAt queda null. | ninguno |
defaults to NOT setting lastContactMessageAt when the flag is omitted | Sin el flag. | lastContactMessageAt queda null. | ninguno |
does NOT overwrite a previously set lastContactMessageAt with an agent message | Primero mensaje del contacto y luego de agente. | lastMessage pasa al segundo mensaje; lastContactMessageAt conserva el valor original. | ninguno |
postpone
| Test | Escenario | Resultado esperado | Mocks |
|---|
sets sendType to postponed and persists postponedUntil | Aplaza un ticket open con fecha futura. | sendType postponed y postponedUntil persistida. | ninguno |
cancelPostpone
| Test | Escenario | Resultado esperado | Mocks |
|---|
sets sendType back to open and unsets postponedUntil | Ticket postponed. | sendType open y postponedUntil undefined. | ninguno |
setAgentUnread
| Test | Escenario | Resultado esperado | Mocks |
|---|
sets agentUnread to true and persists it | setAgentUnread(id, true). | agentUnread true. | ninguno |
sets agentUnread back to false and persists it | Ticket con agentUnread: true. | agentUnread false. | ninguno |
returns null when the ticket does not exist | Id inexistente. | Devuelve null. | ninguno |
findDuePostponed
| Test | Escenario | Resultado esperado | Mocks |
|---|
returns only postponed tickets whose postponedUntil has already passed | Un aplazado vencido, uno futuro y uno open. | Solo incluye el vencido. | ninguno |
reactivatePostponed
| Test | Escenario | Resultado esperado | Mocks |
|---|
CAS: only one of two concurrent reactivation attempts wins | Dos reactivaciones concurrentes del mismo ticket vencido. | Solo una devuelve el ticket (open, sin postponedUntil); la otra devuelve null. | ninguno |
returns null when the ticket is not currently postponed | Ticket open. | Devuelve null. | ninguno |
| Test | Escenario | Resultado esperado | Mocks |
|---|
returns only open/in-progress tickets that have a lastContactMessageAt set | Tickets open y inProgress con actividad, open sin actividad y resolved con actividad. | Incluye los dos con actividad y excluye el que no la tiene y el resuelto. | ninguno |
markAutoResolutionNotified
| Test | Escenario | Resultado esperado | Mocks |
|---|
sets autoResolutionNotifiedAt to the current time | Marca el aviso de auto-resolución. | autoResolutionNotifiedAt no es null. | ninguno |
returns null when the ticket does not exist | Id inexistente. | Devuelve null. | ninguno |
markOutOfHoursNotified
| Test | Escenario | Resultado esperado | Mocks |
|---|
sets outOfHoursNotifiedAt to the current time | Marca el aviso fuera de horario. | outOfHoursNotifiedAt no es null. | ninguno |
returns null when the ticket does not exist | Id inexistente. | Devuelve null. | ninguno |
lets only one of two simultaneous calls claim the notice | Dos llamadas simultáneas. | Solo una devuelve resultado (la otra null). | ninguno |
reopenFromResolved
| Test | Escenario | Resultado esperado | Mocks |
|---|
clears outOfHoursNotifiedAt so the reopened ticket can be notified again | Ticket resolved con ambas marcas de aviso. | Queda open con outOfHoursNotifiedAt y autoResolutionNotifiedAt en null. | ninguno |
| Test | Escenario | Resultado esperado | Mocks |
|---|
returns the existing open ticket instead of creating a new one | El contacto ya tiene un ticket open. | created: false y el mismo ticket. | ninguno |
creates a new ticket when the contact has no open/in-progress/csat-pending ticket | Contacto sin tickets. | created: true y ticket open. | ninguno |
returns a RESOLVED ticket with csat.status=pending when the message content is a rating reply (regression) | Ticket resolved con CSAT pending y mensaje 5. | created: false y devuelve el ticket resuelto. | ninguno |
W2: creates/finds a ticket normally (does NOT match the resolved ticket) when csat.status=pending but the message content is NOT a rating reply | CSAT pending y mensaje que no es una calificación. | created: true y ticket open nuevo. | ninguno |
W2: creates a new ticket when csat.status=pending and no message content is given at all | CSAT pending sin contenido de mensaje. | created: true. | ninguno |
returns a RESOLVED ticket with csat.status=awaiting_comment instead of creating a new one, regardless of message content | CSAT awaiting_comment y cualquier mensaje. | created: false y devuelve el ticket resuelto. | ninguno |
creates a new ticket when the only prior ticket is RESOLVED with csat not_applicable (regression: unaffected contacts) | Único ticket previo resolved sin CSAT. | created: true. | ninguno |
triggerCsat
| Test | Escenario | Resultado esperado | Mocks |
|---|
sets csat.status=pending with the trigger snapshot fields | triggerCsat con triggeredBy, fechas y snapshots de agente y departamento. | csat.status pending con todos los campos persistidos. | ninguno |
recordCsatRating
| Test | Escenario | Resultado esperado | Mocks |
|---|
sets rating, respondedAt and the given next status | CSAT pending, calificación 5, estado answered. | rating 5, status answered y respondedAt correctos. | ninguno |
| Test | Escenario | Resultado esperado | Mocks |
|---|
sets the comment and closes the survey as answered | CSAT awaiting_comment con calificación 4. | comment guardado y status answered. | ninguno |
expireCsatSurveys
| Test | Escenario | Resultado esperado | Mocks |
|---|
expires pending/awaiting_comment surveys past expiresAt and leaves others untouched | Cuatro tickets: pending vencido, awaiting_comment vencido, pending vigente y answered vencido. | Devuelve 2 modificados; los dos vencidos pasan a expired y los otros no cambian. | ninguno |
lockTicket
| Test | Escenario | Resultado esperado | Mocks |
|---|
sets lockedBy when the ticket is currently unlocked and owned by ownerId | Ticket sin bloqueo del dueño. | lockedBy es el dueño. | ninguno |
CAS: returns null when the ticket is already locked (no owner match on lockedBy:null) | Ticket ya bloqueado por otro. | Devuelve null. | ninguno |
returns null when the requester does not own the ticket | Solicitante distinto del dueño. | Devuelve null. | ninguno |
unlockTicket
| Test | Escenario | Resultado esperado | Mocks |
|---|
clears lockedBy when expectedLockedBy matches the current lock owner | expectedLockedBy coincide con el titular. | lockedBy queda null. | ninguno |
CAS: returns null when expectedLockedBy does not match the current lock owner | expectedLockedBy distinto. | Devuelve null. | ninguno |
force unlock (expectedLockedBy=null): clears lockedBy regardless of who holds it | expectedLockedBy null. | Limpia lockedBy sin importar el titular. | ninguno |
auto-clear lockedBy on terminal states
| Test | Escenario | Resultado esperado | Mocks |
|---|
markResolved clears lockedBy | Se resuelve un ticket bloqueado. | lockedBy null. | ninguno |
updateStatus clears lockedBy when the new status is a terminal one (closed) | updateStatus(id, 'closed'). | lockedBy null. | ninguno |
updateStatus clears lockedBy when the new status is cancelled | updateStatus(id, 'cancelled'). | lockedBy null. | ninguno |
updateStatus does NOT clear lockedBy for a non-terminal status (inProgress) | updateStatus(id, 'inProgress'). | lockedBy se conserva. | ninguno |
releaseToQueue clears lockedBy | releaseToQueue(id, owner). | lockedBy null. | ninguno |