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 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()
      Extended findTickets dispatches each of the four new filter arguments (type / state / targetType / updatedSince) through to the DAO unchanged, preserving the state value (it doesn't get coerced into the legacy openOnly path).
    • countTicketsExtended_passesAllFiltersThroughToDao

      @Test public void countTicketsExtended_passesAllFiltersThroughToDao()
      Extended countTickets mirrors findTickets in 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 in TicketServiceImpl.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 in updateMetadata, or named in ownedElsewhere below because another service method writes it. A field whose type this test cannot mint a distinct value for also fails, rather than being skipped.

      title and body live on the superclass and so are not walked; they are covered by TicketPersistenceIT, which asserts title as the control of its copy-list regression tests.

      The sibling test above passes a TRANSIENT ticket, where reattach short-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, and acceptsTargets — 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()