Class TicketServiceImplTest
java.lang.Object
ubic.gemma.persistence.service.common.auditAndSecurity.curation.TicketServiceImplTest
@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class)
public class TicketServiceImplTest
extends Object
Lightweight Mockito unit tests for
TicketServiceImpl covering the
Phase B-1 happy paths (open / assign / addComment / transition) and verifying
that each mutating call appends the correct TicketEventType to the
ticket's append-only event log. The DAO is mocked — these aren't integration
tests; persistence is exercised in a follow-on DAO test once the schema
lands.-
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionvoidvoidvoidIdempotent, NOT a conflict.voidA cancelled ticket is as finished as a resolved one; both refuse target changes.voidState wins over the flag: a finished ticket must not quietly grow new work.voidThe flag is the whole gate.voidvoidvoidExtendedcountTicketsmirrorsfindTicketsin dispatch — each filter argument lands on the DAO without massaging.voidCursor-mode dispatch parity: extended cursor signature forwards each of the four new filter arguments verbatim to the DAO.voidExtendedfindTicketsdispatches each of the four new filter arguments (type / state / targetType / updatedSince) through to the DAO unchanged, preserving thestatevalue (it doesn't get coerced into the legacy openOnly path).voidFirst call mints it.voidThe lookup is scoped by curator, not global.voidvoidAn existing scratchpad is handed back untouched — no second row, no second OPENED event.voidThe duplicate guard as a caller experiences it: call twice, get one ticket.voidThe title names the owner, and a curator whose Contact name was never filled in is named by their username rather than losing their identity to a bare "Scratchpad" (uib, 2026-09-02).voidvoidvoidvoidA completed target may be removed — a scratchpad's rows are all NOT_DONE and refusing would make the common case pay for the rare one.voidRemoving something that is not there has already reached the asked-for state.voidOn a scratchpad, removing IS finishing (Paul, 2026-08-31) — the ticket stays open and the dataset leaves it — so this is the counterpart of addTarget, not an afterthought.voidvoid"Verbatim" excludes text that merely starts with digits: "6 samples" is a title, and reading it as ticket 6 would put an unrelated ticket at the top of the list.voidA ticket whose title is also the number typed would otherwise be listed twice — once as the id match, once as a title match.voidA number that parses but names no ticket is a non-hit, not an error — the caller must not turn it into a 404, because the same string is still a legitimate title search.voidvoidvoidvoidvoidsetUp()voidvoidvoidvoidvoidvoidvoidvoid🛑 The reattach copy list inTicketServiceImpl.updateMetadata(Ticket, String)is hand-maintained, and a field left off it is dropped in silence: the REST layer mutates a DETACHED ticket, the copy skips the field, and the caller is answered 200.voidvoidvoid
-
Constructor Details
-
TicketServiceImplTest
public TicketServiceImplTest()
-
-
Method Details
-
setUp
@BeforeEach public void setUp() -
openTicket_seedsOpenedEvent_andDelegatesToCreate
@Test public void openTicket_seedsOpenedEvent_andDelegatesToCreate() -
openTicket_rejectsEmptyTargets
@Test public void openTicket_rejectsEmptyTargets() -
openTicket_rejectsBlankTitle
@Test public void openTicket_rejectsBlankTitle() -
assign_setsAssignee_andAppendsAssignedEvent
@Test public void assign_setsAssignee_andAppendsAssignedEvent() -
assign_withNullAssignee_clearsAssignment_andStillLogsEvent
@Test public void assign_withNullAssignee_clearsAssignment_andStillLogsEvent() -
addComment_appendsCommentedEvent_withPayload
@Test public void addComment_appendsCommentedEvent_withPayload() -
transition_OPEN_to_IN_PROGRESS_emitsStateChanged
@Test public void transition_OPEN_to_IN_PROGRESS_emitsStateChanged() -
transition_IN_PROGRESS_to_RESOLVED_emitsResolved
@Test public void transition_IN_PROGRESS_to_RESOLVED_emitsResolved() -
transition_OPEN_to_CANCELLED_emitsCancelled
@Test public void transition_OPEN_to_CANCELLED_emitsCancelled() -
transition_RESOLVED_to_OPEN_emitsReopened
@Test public void transition_RESOLVED_to_OPEN_emitsReopened() -
transition_noop_is_silent
@Test public void transition_noop_is_silent() -
terminalStatesMapToTypedEvents_table
@Test public void terminalStatesMapToTypedEvents_table() -
findTicketsExtended_passesAllFiltersThroughToDao
@Test public void findTicketsExtended_passesAllFiltersThroughToDao()ExtendedfindTicketsdispatches each of the four new filter arguments (type / state / targetType / updatedSince) through to the DAO unchanged, preserving thestatevalue (it doesn't get coerced into the legacy openOnly path). -
countTicketsExtended_passesAllFiltersThroughToDao
@Test public void countTicketsExtended_passesAllFiltersThroughToDao()ExtendedcountTicketsmirrorsfindTicketsin dispatch — each filter argument lands on the DAO without massaging. -
findTicketsByCursorExtended_passesAllFiltersThroughToDao
@Test public void findTicketsByCursorExtended_passesAllFiltersThroughToDao()Cursor-mode dispatch parity: extended cursor signature forwards each of the four new filter arguments verbatim to the DAO. -
updateMetadata_bumpsUpdatedAt_andDoesNotAppendTicketEvent
@Test public void updateMetadata_bumpsUpdatedAt_andDoesNotAppendTicketEvent() -
updateMetadata_copiesEveryMutableMetadataField_offADetachedTicket
@Test public void updateMetadata_copiesEveryMutableMetadataField_offADetachedTicket() throws Exception🛑 The reattach copy list inTicketServiceImpl.updateMetadata(Ticket, String)is hand-maintained, and a field left off it is dropped in silence: the REST layer mutates a DETACHED ticket, the copy skips the field, and the caller is answered 200. payload / payloadSchemaVersion shipped that way until 2026-09-04 and acceptsTargets until 2026-09-11.This walks
Ticket's own fields and fails on any one that does not survive the call, so a field added to the entity has to be classified deliberately — copied inupdateMetadata, or named inownedElsewherebelow because another service method writes it. A field whose type this test cannot mint a distinct value for also fails, rather than being skipped.titleandbodylive on the superclass and so are not walked; they are covered byTicketPersistenceIT, which assertstitleas the control of its copy-list regression tests.The sibling test above passes a TRANSIENT ticket, where
reattachshort-circuits and the copy list never runs — which is why it cannot see any of this.- Throws:
Exception
-
getOrCreateScratchpad_createsOneOnFirstCall
@Test public void getOrCreateScratchpad_createsOneOnFirstCall()First call mints it. The two properties asserted here are the ones that make it a scratchpad rather than an ordinary ticket: the type, andacceptsTargets— a scratchpad nothing can be added to is inert, and the flag defaults to false. -
getOrCreateScratchpad_titlesItByUsername_whenTheContactNameIsMissing
@Test public void getOrCreateScratchpad_titlesItByUsername_whenTheContactNameIsMissing()The title names the owner, and a curator whose Contact name was never filled in is named by their username rather than losing their identity to a bare "Scratchpad" (uib, 2026-09-02). -
getOrCreateScratchpad_returnsTheExistingOne_withoutCreating
@Test public void getOrCreateScratchpad_returnsTheExistingOne_withoutCreating()An existing scratchpad is handed back untouched — no second row, no second OPENED event. -
getOrCreateScratchpad_secondCallReturnsTheSameTicket_andCreatesOnlyOnce
@Test public void getOrCreateScratchpad_secondCallReturnsTheSameTicket_andCreatesOnlyOnce()The duplicate guard as a caller experiences it: call twice, get one ticket. The DAO stub stores what create() was handed and hands it back to the next findScratchpad, which is what a committed row does. Nothing in the schema enforces this — see TicketService#getOrCreateScratchpad for the race that survives — but the query-then-create path itself must not mint a second one. -
getOrCreateScratchpad_isScopedToTheCurator
@Test public void getOrCreateScratchpad_isScopedToTheCurator()The lookup is scoped by curator, not global. One curator already has a scratchpad and the other does not: the first must get theirs back and the second must get a new one of their own. A lookup that ignored the curator would hand the second curator the first one's, or miss the first's entirely. -
getOrCreateScratchpad_rejectsANullCurator
@Test public void getOrCreateScratchpad_rejectsANullCurator() -
addTarget_appendsTheTarget_andLogsTargetAdded
@Test public void addTarget_appendsTheTarget_andLogsTargetAdded() -
addTarget_refusesWhenTheTicketDoesNotAcceptAdditions
@Test public void addTarget_refusesWhenTheTicketDoesNotAcceptAdditions()The flag is the whole gate. False is the default and the state of every ticket that predates it, so without this check an agent-created ticket's fixed batch could silently grow. -
addTarget_refusesOnAResolvedTicketEvenWithTheFlagOn
@Test public void addTarget_refusesOnAResolvedTicketEvenWithTheFlagOn()State wins over the flag: a finished ticket must not quietly grow new work. -
addTarget_isIdempotentOnADuplicate
@Test public void addTarget_isIdempotentOnADuplicate()Idempotent, NOT a conflict. uib's argument, which is the deciding one: the client cannot know membership at click time — a menu may have been open for a minute — and a curator clicking twice must not get an error for reaching the state they asked for, nor a duplicate row on a 500-target ticket. -
addTarget_refusesOnACancelledTicket
@Test public void addTarget_refusesOnACancelledTicket()A cancelled ticket is as finished as a resolved one; both refuse target changes. -
removeTarget_removesMembership_andLogsTargetRemoved
@Test public void removeTarget_removesMembership_andLogsTargetRemoved()On a scratchpad, removing IS finishing (Paul, 2026-08-31) — the ticket stays open and the dataset leaves it — so this is the counterpart of addTarget, not an afterthought. -
removeTarget_isIdempotentWhenAbsent
@Test public void removeTarget_isIdempotentWhenAbsent()Removing something that is not there has already reached the asked-for state. -
removeTarget_allowsRemovingCompletedWork_butReportsIt
@Test public void removeTarget_allowsRemovingCompletedWork_butReportsIt()A completed target may be removed — a scratchpad's rows are all NOT_DONE and refusing would make the common case pay for the rare one. The status comes back so the caller can say what went. -
updateTargetStatus_appendsTargetStatusChangedEvent_andBumpsUpdatedAt
@Test public void updateTargetStatus_appendsTargetStatusChangedEvent_andBumpsUpdatedAt() -
updateTargetStatus_noOp_whenAlreadyAtRequestedStatus
@Test public void updateTargetStatus_noOp_whenAlreadyAtRequestedStatus() -
updateTargetStatus_unknownTargetId_throwsIAE
@Test public void updateTargetStatus_unknownTargetId_throwsIAE() -
searchTickets_listsTheVerbatimIdMatchFirst
@Test public void searchTickets_listsTheVerbatimIdMatchFirst() -
searchTickets_doesNotListTheIdMatchTwiceWhenItAlsoMatchesTheTitle
@Test public void searchTickets_doesNotListTheIdMatchTwiceWhenItAlsoMatchesTheTitle()A ticket whose title is also the number typed would otherwise be listed twice — once as the id match, once as a title match. -
searchTickets_idThatNamesNoTicket_yieldsNoHitRatherThanAnError
@Test public void searchTickets_idThatNamesNoTicket_yieldsNoHitRatherThanAnError()A number that parses but names no ticket is a non-hit, not an error — the caller must not turn it into a 404, because the same string is still a legitimate title search. -
searchTickets_nonNumericQuery_neverRunsTheIdLookup
@Test public void searchTickets_nonNumericQuery_neverRunsTheIdLookup() -
searchTickets_digitsFollowedByText_isTitleTextNotAnId
@Test public void searchTickets_digitsFollowedByText_isTitleTextNotAnId()"Verbatim" excludes text that merely starts with digits: "6 samples" is a title, and reading it as ticket 6 would put an unrelated ticket at the top of the list. -
searchTickets_passesOpenOnlyAndTheCallersContactIdToBothQueries
@Test public void searchTickets_passesOpenOnlyAndTheCallersContactIdToBothQueries() -
searchTickets_truncatesToTheLimit
@Test public void searchTickets_truncatesToTheLimit() -
searchTickets_blankQuery_returnsNothingAndQueriesNothing
@Test public void searchTickets_blankQuery_returnsNothingAndQueriesNothing()
-