Developer Tests Tickets - edmozley/freeitsm GitHub Wiki

๐Ÿงช Developer Tests โ€” Tickets & tasks

Part of Developer Tests. Ticket numbering, saved views, and the task engine: priority, recurrence, the people on a task, and what goes into a calendar.

Test Needs
ticket-numbering.php Database
mailbox-department.php Database + HTTP
ticket-merge-undo.php Database
table-views.php Database
team-counts-no-departments.php Database
analyst-folders-by-company.php Database, two or more companies
tasks-priority.php Nothing
task-recurrence-dates.php Nothing
task-recurrence-spawn.php Database
task-collaborators/run.php Database
calendar-sync-tasks.php Database
task-evidence.php Database (ZZ-EV series, removed)
live-pulse.php A disposable install: signatures through the service, then the real endpoint (forged session); a ZZ analyst, task, note, history row and notification, removed; the ticket's updated time put back
ticket-forms.php A disposable install: the service, the browser endpoint (forged session), the case report and the REST API (real keys); ZZ form, analyst, keys and history rows removed
ticket-watchers.php A disposable install: service, notifications, the browser endpoint (forged sessions) and the REST API (real keys); ZZ analysts, keys, watches and notifications removed
checklists-api.php A disposable install, through /api/v1 with real keys (ZZ analyst, keys, template, task and notes, removed)
ticket-checklists.php A disposable install, through its real endpoint (a ZZ analyst, template and notes, removed)
task-read-only.php Database (a Read only grant, removed)
task-forms.php Database (ZZ-TF form and tasks, removed)
task-evidence-atomic.php Database (ZZ-ATOM tasks; a temporary trigger with a base URL, dropped)
case-report.php Database (ZZ ticket, task and analyst, in a rolled-back transaction)
The workspace and table suites (nine files) Nothing / Chrome / a disposable install - see the section
ticket-type-rules.php Database (ZZ230 types, rules, tickets and a second company, removed)
ticket-reply-to.php Chrome or Edge, no database
email-thread-replies.php Nothing; the database for the match and importer checks (a ZZ mailbox and emails, removed)
task-drag-notifies.php A running install over HTTP (tagged rows, removed)
emails-lookup-indexes.php Nothing; the database for the EXPLAIN checks
test_email_thread.php โš ๏ธ Not a test โ€” see below

tests/ticket-numbering.php

What it tests

The assertions that matter are not "it makes a number". They are:

  1. A number is never issued twice โ€” including one a renumbered ticket used to have, because a reply quoting it must not land on a stranger's ticket.
  2. An old number keeps resolving forever, because the emails quoting it live in customers' inboxes and nobody can recall them.
  3. The reference parser recognises any format, because an install can change its format and every number it ever issued has to go on working.
  4. The padding is a minimum, never a limit.

How it works

Creates only ZZNUM-prefixed rows and sweeps them before and after. It exercises the renumber path deliberately, because that is where a number can be freed and handed out again.

Run it

php tests/ticket-numbering.php

If it fails

A reissued number is the serious one. It routes a customer's reply onto somebody else's ticket, which is a data leak dressed as a filing error. Treat any red here as blocking a release.


tests/table-views.php

What it tests

Saved table views โ€” specifically the visibility rules. There are three answers (mine, my team's, everyone's) and a fourth that must never happen: somebody else's private view.

Each is checked from the side that must be refused as well as the side that must work, using a refuses() helper that asserts the call throws.

Run it

php tests/table-views.php

Creates ZZTV-named rows and removes them, including on failure.

If it fails

A refuses() assertion going red means one analyst can load another's private view โ€” including its filters, which can describe data they cannot otherwise see.


tests/team-counts-no-departments.php

What it tests

The ticket folder numbers for an analyst who is in a team. Someone in a team gets folders and counts for that team's departments, plus every ticket with no department. Until 3.7.0 a team linked to no department set every count to 0 instead โ€” the bug in A team with no departments showed 0 tickets.

  1. A team with no departments โ€” All Tickets equals the number of tickets with no department, the status folders are counted, there are no department folders, and the analyst's own folder is unchanged.
  2. A team with one department โ€” All Tickets is that department plus the no-department tickets, and the departments the team is deliberately not linked to stay out. This half guards the other direction: a fix for (1) that simply stopped filtering would pass (1) and fail here.

Two positive controls stop it passing vacuously: there must be tickets with no department, and tickets in a department the team is not linked to.

How it works

It drives the real endpoint, api/tickets/get_ticket_counts.php, in-process, as an active analyst who is in no team. The endpoint opens its own database connection, so a rolled-back transaction would be invisible to it โ€” instead the test writes one throwaway team (ZZ test team counts) and its links, deletes them by id in a finally, and then checks they are gone. It writes nothing else.

COUNTS_ENDPOINT=<path> points it at another copy of the endpoint. That is how the control was run: against the 3.6.1 code it fails exactly the two "no departments" counting checks and passes the rest.

Run it

php tests/team-counts-no-departments.php

If it fails

A red "no departments" check means somebody has short-circuited an empty department list to 0 again โ€” look for the TRAP: line in get_ticket_counts.php. A red "one department" check is the more serious one: team members are seeing tickets in departments their team is not linked to.


tests/analyst-folders-by-company.php

What it tests

Which analysts get a folder in the inbox's Analyst grouping on a multi-company install (#176). Until 3.7.0 it was every active analyst, so one client's analysts saw another client's by name. The rule now: an analyst gets a folder if they can access the company being viewed (with All companies, any company the viewer can access), or they are the viewer, or they hold tickets in scope.

  1. Viewing the company the analyst can access - they get a folder.
  2. Viewing a company they cannot access - no folder, while the viewer keeps their own.
  3. All companies - they get a folder, because they share a company with the viewer.
  4. Nobody who can access the company lost their folder - checked against every real analyst, so a fix that hid too much fails here.

How it works

Drives the real api/tickets/get_ticket_counts.php in-process. The analyst under test is a throwaway (zz-test-folders) who can access exactly one company, with an unusable password hash so nobody can sign in as it; it is deleted by id in a finally and checked gone. Skips on a single-company install, where the list is deliberately unchanged. Control: against the code before #176 it fails check 2 and passes the rest.

Not covered: the "holds tickets in scope" exception (it would mean assigning a real ticket to the throwaway analyst).

Run it

php tests/analyst-folders-by-company.php

If it fails

Check 2 red means analysts' names are shown to other companies again. Check 4 red means analysts who should be there have vanished - usually a copy of the company-access rule that has drifted from getAccessibleTenantIds() (there is a TRAP: line about exactly that).


tests/tasks-priority.php

What it tests

This is a scan, not a behaviour test, and deliberately so.

The bug it guards is not a wrong output โ€” it is a renderer deriving a colour from a priority's display name. That produced class="priority-dot hoch" on a German install, matched none of the four hardcoded English rules, and drew a transparent circle: no error, no fallback, nothing on screen to suggest a setting had not applied.

A behaviour test proves the renderer that exists today is right. It cannot stop the fifth renderer, written next year, from reaching for the name again because that is what its neighbours used to do. This is the same shape as the bug where every ticket intake path resolved its status by the word "Open", and the Watchtower counters before that. Three times, so the invariant gets a test rather than a comment:

  1. No stylesheet carries a per-priority-name rule.
  2. No renderer interpolates a priority into a class attribute.
  3. Every place that draws a priority goes through TasksPriority.
  4. TasksPriority validates the colour and escapes the name โ€” both are admin-editable free text, and the name is stored exactly as typed.

Run it

php tests/tasks-priority.php

48 assertions. No database, no network, writes nothing.

If it fails

Someone has added a renderer that builds a class from a priority name, or a stylesheet rule keyed to one. Route it through TasksPriority instead. The test names the file.


tests/task-recurrence-dates.php

What it tests

The recurrence date engine. Pure date arithmetic, no database โ€” which is precisely why it is worth testing hard: every interesting bug in a recurrence feature is a calendar edge case, and none of them show up in a demo where everything falls on the 15th of a 31-day month.

The cases are the ones that break naive implementations:

  • the 31st in a 30-day month, and in February
  • "the last day of the month" across month lengths and a leap year
  • "the 5th Tuesday" of a month that has only four
  • "every 2 weeks on Mon and Thu" not collapsing into every week
  • 29 February under a yearly rule

Run it

php tests/task-recurrence-dates.php

60 assertions, nothing required. Each line prints the date it produced, so a failure shows you the arithmetic rather than just a label.

If it fails

The output gives got and wanted as real dates. Work out which rule the case belongs to before changing anything โ€” these cases interact, and "fixing" the 31st commonly breaks the last-day-of-month rule.


tests/task-recurrence-spawn.php

What it tests

The other half of recurrence: that completing a task actually produces the next one, carries across what the rule says to carry and nothing it should not, and fires from both ways a task can be completed.

๐Ÿ”‘ That last one is the point. TasksService::moveTask โ€” dragging a card into a closed column โ€” is documented "No workflow event" and dispatches nothing. A hook placed only on saveTask would give you a task that repeats when you tick it and silently does not when you drag it. Dragging is the commoner action.

Run it

php tests/task-recurrence-spawn.php

ZZREC-prefixed, cleaned up including on failure.

If it fails

If only the drag path is red, the hook has been attached to one completion route again. Both routes must spawn.


tests/task-collaborators/run.php

What it tests

The other people on a task โ€” the "Involved" list.

๐Ÿ”ด The first suite is the one that matters. A task you are on that fails to appear in one list is indistinguishable, to the person looking, from your never having been added to it โ€” and there are four separate places that can hide it. A single assertion against api/tasks/list.php would pass while the REST API still hid the task, so every surface is asserted separately.

โš ๏ธ Every "it works" assertion is paired with a control proving the check can actually fail. A filter test that passes because the query returns everything is not evidence of anything.

Run it

php tests/task-collaborators/run.php

Writes one task and two throwaway analysts, all removed in the teardown.

If it fails

The label names the surface. Fix it there, then ask whether the other three share a helper that should have been used.


tests/calendar-sync-tasks.php

What it tests

Tasks in a calendar โ€” the decisions, not the network.

๐Ÿ”‘ Nothing here talks to Microsoft. What is worth testing is what the code decides: which of a task's two dates belongs in whose calendar, what an event looks like once built, and what happens at the edges. The provider is a network call and belongs behind a live run, not a unit test.

Run it

php tests/calendar-sync-tasks.php

ZZCAL-named rows, removed including on failure.

If it fails

Read it as "we would now put the wrong thing in somebody's calendar". Since the network half is deliberately untested here, a green run does not mean sync works end to end โ€” that still needs a live run against a real account.


tests/task-forms.php

What it tests

Forms on tasks (#190): a ZZ-TF form on ZZ tasks. The requirement uses the form's newest version; the one completion gate blocks, warns or ignores it; the shared draft's revision turns a stale save into a conflict naming who saved; a stale submit is a conflict that leaves no submission behind; Forms validation still refuses a missing answer; a retry with the same client reference returns the same submission; a correction supersedes; the evidence cannot be deleted from Forms (with an ordinary submission as the control); the repeat copies the requirement unanswered; the #181 snapshot and evidence report carry the answers - and not to a viewer without Forms or the permission; company scope hides them from another company.

Run it

php tests/task-forms.php http://localhost/freeitsm-app

With a base URL it also drives api/tasks/forms.php, the conflict over HTTP and the form answers CSV. It deletes what it made with SQL, because deleteSubmission() and deleteForm() now refuse task evidence. Details: Forms on tasks - Developer Guide.


tests/task-evidence.php

What it tests

Repeating task evidence (#181), end to end: a ZZ-EV fixed-schedule series with one occurrence of every outcome - on time; Dr4inder's due the 1st, moved to the 8th, done the 7th (on time after rescheduling, not on time); a due date moved after completion (flagged, never re-judged); late; skipped with a reason; removed with a failed checklist step that survives in the report; overdue; a month never created (missing, explained as run history not available - never "did not run"). It asserts the report creates nothing, that the delete setting refuses, and on an after-completion series that completing makes one successor, reopening and completing again makes no second, and a successor that was expected and never appeared is missing and names the completion that made it due.

Run it

php tests/task-evidence.php http://localhost/freeitsm-app

With a base URL it also drives the board's drag (api/tasks/reorder.php) over HTTP and checks it now makes the next occurrence - it never did before #181.

If it fails

Read it as "the evidence report would tell an auditor something untrue". It deletes its history rows too - they have no foreign keys and would otherwise appear in Tasks โ†’ Evidence as a real series. See Repeating task evidence - Developer Guide.


tests/task-read-only.php

What it tests

Read only access to Tasks (#181 Part G): a restricted analyst given Tasks at the Read only level can read a task and its history, and every write - create, edit, move, comment, delete, skip - is refused by the service and, with a base URL, by the endpoints, including the board's drag. The board marks itself read only. Positive controls: an administrator is full and can create.

Run it

php tests/task-read-only.php http://localhost/freeitsm-app

The ticket workspace and ticket table suites (#211-#213)

Contributed by @Dr4inder with the ticket workspace and the ticket table. Each guide's testing section says what every check covers: workspace, table, task editor.

Test Needs Checks
ticket-workspace-settings.php Nothing 20
task-embed-bridge.php Chrome 11
data-table-server.php Chrome 9
data-table-row-open.php Chrome 10
ticket-workspace.php Chrome 53
links-strip.php Chrome 19
ticket-workspace-actions.php Chrome 27
ticket-workspace-api.php Disposable install 24
task-embedded-workspace.php Disposable install 13
ticket-workspace-table.php Disposable install 24
ticket-workspace-live.php, ticket-task-live.php, ticket-table-live.php Disposable install + Chrome 16, 20, 12

ticket-workspace-live.php failed "native internal note saves" from before 4.12.0 until 5.1.0 (#2824): it set the note box's .value, but #noteText has been an editable area since pictures in notes (GH #224), so the note was empty and refused. It now sets textContent. Notes themselves were never broken - when a browser test fails on typing, check the element is still a form field before suspecting the feature.

Run it

php tests/ticket-workspace-settings.php
FREEITSM_TEST_URL=http://localhost/freeitsm-prtest FREEITSM_TEST_DATABASE=freeitsm_prtest php tests/ticket-workspace-api.php

The install suites create companies, analysts, tickets and tasks (ZZ names) and delete them by id in finally. They refuse to run unless FREEITSM_TEST_URL is on localhost and FREEITSM_TEST_DATABASE names the database that install actually uses - so they cannot be pointed at the wrong one by accident. Do not point them at an install with real data. A disposable one: git worktree add ../freeitsm-prtest, a config.php there that loads its own db_config.php, and a new database loaded from database/freeitsm.sql. Chrome is found automatically, or set CHROME_BIN.

If it fails

  • "currentEmail is not defined", "TicketWorkspace is not defined" on every live check: the page never loaded - usually the login failed. Check FREEITSM_TEST_URL opens the login page.
  • The live suites passed on the review's install at a subfolder (/freeitsm-prtest/) only after their web-root URLs were fixed; a new check must not assume the web root either.

tests/test_email_thread.php โ€” deleted in 2.3.1

โš ๏ธ It was never a test. It was a leftover scratch page: it rendered the email thread of ticket #45, hardcoded, as an HTML page, with no assertions and no pass/fail count. It was written to look at thread formatting.

It also had no authentication check of any kind โ€” it called session_start() and then printed that ticket's from_address and body_content to whoever asked. Until tests/ was closed off it was fetchable over HTTP by anyone who knew the path. See Test suite exposure.

It is recorded here so nobody goes looking for it, and so nobody mistakes its former existence for coverage of email threading. There is none โ€” email threading has no automated test.


tests/ticket-type-rules.php

4.8.0, #230. The type a new ticket gets when nobody chose one. 33 checks.

  • Part 1 (no database): what a rule's criteria keep - unknown channels and keys dropped, ids cleaned, words lower-cased once each, a domain typed with @ or a trailing dot cleaned - and when they match: every part filled in, any of several ticks, an unknown fact (no department yet) not matching, subject words in any case, a domain and its subdomains but not a look-alike.
  • Part 2 (one company): the first matching rule decides, the next when it does not match; a rule that is off or names a retired type is passed over; the default; a ticket made through the real TicketsService::createTicket() gets the rule's type and its history names the rule; a type the caller chose always wins; an API ticket counts as REST API; Workflow's set_ticket_type changes a type and refuses one the company cannot use.
  • Part 3 (two companies, in a child process - the company count is cached per process): a company's own rules before the shared ones; a shared rule naming a type the company hid is passed over for it; another company's type is never given; a triage ticket only gets shared types; a company's own default, "none", and the shared one.

Needs a single-company test database; it makes the second company itself and removes it, with every ZZ230 type, rule, ticket and setting, in finally.

FREEITSM_TEST_DATABASE=<throwaway db> php tests/ticket-type-rules.php

See A type for every new ticket - Developer Guide.


tests/ticket-reply-to.php

What it tests

Tickets -> Settings -> General -> Reply goes to (#120). In PHP: the setting belongs to the General tab, only message and requester exist, the save endpoint validates it, and the inbox is told the choice. In headless Chrome, the shipped replyAddress() and correspondentAddress() from assets/js/inbox.js: with requester A and B's message open, Message answers B and Requester answers A; an unknown or missing setting behaves as Message; our own outbound message answers the requester in both; a requester with no address falls back to the message's person; a message sent only to an analyst still answers the requester (#244).

Run: php tests/ticket-reply-to.php - 12 checks, no database.


tests/email-thread-replies.php

What it tests

Tickets -> Settings -> General -> A reply without a ticket number (#120; developer guide).

Without a database (6 checks): the Basic IMAP thread key - References root, then In-Reply-To, then the message's own Message-ID, none -> no key - and imapReplyHeaders() reading folded header lines under a lower-case FETCH key.

With FREEITSM_TEST_DATABASE (9 more): emailThreadTicket() finds a conversation's ticket, not from another mailbox, nothing for an unknown conversation, a Basic IMAP original stored before thread keys (by Message-ID, and by In-Reply-To), and a merged ticket's survivor. Then the real saveEmailToDatabase(), lifted out of the endpoint by token as mailbox-check-overlap.php does, with the setting on: a Graph-shaped reply with no [SDREF] joins the existing ticket, is stored with its thread key and as a reply (is_initial = 0).

php tests/email-thread-replies.php
FREEITSM_TEST_DATABASE=<the throwaway db> php tests/email-thread-replies.php

15 checks. Needs Database Verify run first (it says so if emails.thread_key is missing). It adds a ZZ mailbox pair and ZZ emails and joins two existing tickets, and removes the mailboxes, their emails, their email_import log rows and the setting in finally - by mailbox, so a row the importer saved before a failure goes too. No ticket is created.


tests/task-drag-notifies.php

What it tests

That moving a task to another status by dragging it on the board tells the people on it (#266). Two synthetic analysts sign in for real; one drags the other's task through the real api/tasks/reorder.php. Checks: both the drag and the REST move call announceStatusMove(); To Do -> In Progress notifies the owner; a reorder within the column notifies nobody; -> Done notifies again; the person who dragged it is not notified about their own move.

FREEITSM_TEST_URL=http://localhost/<install> FREEITSM_TEST_DATABASE=<its db> php tests/task-drag-notifies.php

7 checks. Control: the old reorder.php fails 4. Everything it makes is tagged zz-drag-... and removed in finally.


tests/emails-lookup-indexes.php

What it tests

That the two lookups every install makes on emails all day are indexed (#280): the duplicate check by exchange_message_id and the conversation by ticket_id. Statically, that both indexes are in freeitsm.sql and in Database Verify's list; with FREEITSM_TEST_DATABASE set to the database config.php uses, that EXPLAIN shows MySQL using them rather than scanning (run Database Verify first).

php tests/emails-lookup-indexes.php                                   # 4 static checks
FREEITSM_TEST_DATABASE=<its db> php tests/emails-lookup-indexes.php   # 6

tests/ticket-checklists.php

Ticket checklists on the shared engine (#271), through the real api/tickets/ticket_checklists.php as a ZZ analyst with a forged session: attaching copies steps in order with their description and result mode and records who attached it; the server refuses a required value left empty and a result step being just ticked; an unanswered mandatory result step is outstanding at closure; Fail needs a note, ticks the step and does not hold up closing; a plain step refuses a result and an unknown result is refused; Clear removes the result; Not applicable needs no note; each change writes its note on the ticket naming the analyst (and a refused tick writes none - the control). At service level, a workflow skips a template already attached, attaches naming no analyst, and its tick is noted as System.

FREEITSM_TEST_DATABASE=<throwaway db> FREEITSM_TEST_URL=http://localhost/<that install> php tests/ticket-checklists.php

26 checks. Runs Database Verify first. Everything it makes - template, checklists, notes, the analyst and their session file - is removed in finally.


tests/checklists-api.php

The REST API's checklists (#273) through the real front controller with three real keys (read + update, read only, none): both GETs answer and in exactly the same shape; steps say what they ask for, with the prompt, description and closure; a key without the resource is 403 and a read-only key cannot write; a step addressed through another ticket - or a ticket's step through a task - is 404; a missing value, both completed and result, a non-boolean completed and a fail without a note are 422; answers come back as the step; the ticket's note is the key's analyst's and ends (via the API), and the task step history records via: api; the task's template_version is its real value, not null (changelog #2800 - it was never selected); with checklists on tasks off, GET is empty and PATCH is 422 disabled.

FREEITSM_TEST_DATABASE=<throwaway db> FREEITSM_TEST_URL=http://localhost/<that install> php tests/checklists-api.php

21 checks. Runs Database Verify first; puts the tasks-checklists setting back.


tests/live-pulse.php

Live updates (#267). Two looks in a row agree; every topic answers a hash. A ticket's updated time moves the list and that ticket but not the board; a note moves both; a history row moves the ticket; binning and restoring with no history row (the REST API path) moves the list and back. A new task moves the board, a comment moves the open task, deleting it moves the board and the task answers null. A notification moves the bell. Nonexistent ticket and task are null, unknown topics are dropped, at most 12 topics a request; the heartbeat preference is 15 by default and 0 when switched off. The endpoint returns hashes only (no dates, ids or counts) and refuses a signed-out caller. See Live updates - Developer Guide ยง9 for the browser checks.

FREEITSM_TEST_DATABASE=<throwaway db> FREEITSM_TEST_URL=http://localhost/<that install> php tests/live-pulse.php

22 checks. Runs Database Verify first.

tests/ticket-forms.php

Forms on a ticket (#272). Off: GET says so and lists nothing; adding is refused; a required form left in the table does not hold up the close. Adding: the list offers only the newest version of each active form; adding version 1 fills version 2; the same form twice is refused; Form added in the ticket history; a ticket that does not exist is not_found. The close: block refuses and names the form, warn does not. Filling (the fill page's own calls): host is ticket; a draft moves the revision; a stale draft is a conflict naming who saved; a submit missing a required answer is refused; the submission is linked by for_ticket_id (not task_id) on the newest version; Form submitted in the history; the close lets go; the panel shows the answers. The ticket permission (not the task one) grants answers. Forms refuses deleting the submission (ticket_evidence). The case report carries the answers. REST: list with answers; read-only key refused; submit 201, replay 200 with the same submission; a requirement addressed through the wrong ticket 404. Removing keeps both submissions in Forms; merge moves forms once each.

FREEITSM_TEST_DATABASE=<throwaway db> FREEITSM_TEST_URL=http://localhost/<that install> php tests/ticket-forms.php

30 checks. Runs Database Verify first; puts the setting back.

tests/ticket-watchers.php

Watching a ticket (#277). Rules: anyone who can see a ticket can watch it (read-only too); watching twice is one watch; adding a colleague needs write access; an inactive analyst cannot be added; the list is oldest first. Who is told (through the real notificationsHandleEvent()): a note reaches the assignee and the watcher, never the watcher who wrote it; a reassignment reaches the new assignee as ticket.assigned and a watcher as ticket.reassigned; with notes unticked in Settings the watcher hears nothing about a note while the assignee still does; never saved = every event. Browser endpoint (forged session): GET, Stop watching, watching a selection with a ticket that does not exist skipped, 404 for it. REST API (real keys): list; read-only key refused; POST as the key's analyst 201 then 200; DELETE then 404. A watcher who is deactivated (changelog #2802): never saved = stop; under stop the watch is kept but not listed and a note tells them nothing, and it is back once they are made active again; under remove, deactivating through AnalystsService::update() and through analystDeactivate() (the leaving screen) both take them off. Merge moves watchers once each.

FREEITSM_TEST_DATABASE=<throwaway db> FREEITSM_TEST_URL=http://localhost/<that install> php tests/ticket-watchers.php

33 checks. Puts ticket_watch_events and ticket_watch_inactive back. It found that analystCanAccessTicket() is a company check that is true for any id on a single-company install - so TicketWatchers::canSee() also checks the ticket exists.

tests/ticket-merge-undo.php

Undo merge gives the returning ticket back what the merge deleted as a duplicate, not just what it moved (5.1.0, write-up). Two ZZ tickets and ZZ analysts: one watches both tickets, one only the merged-away ticket, one only the survivor. Merges, checks the survivor has each watcher once and the merge recorded the dropped row, undoes, and checks both tickets have exactly their own watchers again. 5 checks; with the 5.0.0 includes/ticket_merge.php two fail. Everything is removed in finally.

FREEITSM_TEST_DATABASE=<throwaway db> php tests/ticket-merge-undo.php

tests/mailbox-department.php

A department per mailbox (#315). Saves an IMAP mailbox through the real save_mailbox.php (no department, then one, then saving without the field keeps it, and an inactive department newly chosen is refused), then checks ticketDepartmentForMailbox(): the mailbox's department; with none, exactly the company's default as before; a department switched off since falls back to the default with a history note; and deleting the department returns the mailbox to the default (ON DELETE SET NULL). 11 checks; everything named ZZMB removed in finally.

FREEITSM_TEST_DATABASE=<throwaway db> FREEITSM_TEST_URL=http://localhost/<that install> php tests/mailbox-department.php

Related

โš ๏ธ **GitHub.com Fallback** โš ๏ธ