Developer Tests Integrations - edmozley/freeitsm GitHub Wiki
π§ͺ Developer Tests β Integrations & external services
Part of Developer Tests. Issue trackers, AI providers, CalDAV, Microsoft Graph mail folders, and the inventory agent's ingest endpoints.
π The recurring idea on this page: test the decision, not the network. Most
of these suites deliberately stop at the boundary β they prove what FreeITSM
sends and decides, using a fixture or a local stand-in, and say plainly what
they cannot prove. A green run here is not a working integration; it is a correct
request. The exception is caldav-provider.php, which really does drive a server.
| Test | Needs |
|---|---|
integrations/run.php |
Nothing |
integrations/templates_check.php |
Database |
azure-openai/run.php |
Nothing (starts its own server) |
caldav-provider.php |
A running Baikal fixture |
mailbox-folder-resolve.php |
Nothing |
agent-empty-report-guard.php |
Database |
tests/integrations/run.php
What it tests
IssueDoc and the provider contract for external issue trackers.
IssueDoc is the piece every later phase writes through β descriptions first,
comments next, everything after. If it is wrong, the next phase is where you find
out and where you would have to rip it up. So it gets a real suite now rather
than an eyeball.
Four renderers produce four genuinely different syntaxes from one document, and the risk is that they drift β one quietly stops escaping, or handles an empty paragraph differently. So every case runs through all four and each is asserted separately.
β οΈ Every negative assertion is paired with a positive control. "The asterisk did not become italics" proves nothing on its own β it is equally true of a renderer that dropped the text entirely. So each escaping test also asserts the surrounding content still arrived.
Run it
php tests/integrations/run.php
353 assertions. No database, no network, pure functions β fast and safe to run anywhere. This is the best test to run first on a machine you have just cloned to.
Reading the result
Ends with a passed: / failed: block rather than the one-line summary most
other tests use.
If it fails
The label names the renderer and the case. Fix the one renderer β do not "normalise" all four to agree, because they are supposed to differ; they target different syntaxes.
tests/integrations/templates_check.php
What it tests
That every workflow starter recipe can actually run.
workflow/includes/templates.php promises in its own header that "every
trigger_event here is a real, wired trigger β¦ and every action type is a real
handler β a recipe that can't actually run would be worse than no recipe at
all". Nothing enforced that, and the first tracker recipe shipped with an
add_note action that does not exist (it is add_ticket_note).
For every template, not just the tracker ones:
- the trigger exists
- every action type exists
- every arg name exists on that action
- every required arg is supplied, or is a
$configuremarker the user fills in - it resolves against this install without throwing
Run it
php tests/integrations/templates_check.php
β οΈ Separate from run.php on purpose. WorkflowEngine::availableActions()
reads webhook formats from the database, and run.php's whole value is that it
needs no database and no network. Do not merge this back into it.
If it fails
A recipe offers an action or an argument that does not exist. Someone renamed a handler without updating the templates β fix the template, since the handler name is the real one.
tests/azure-openai/run.php
What it tests
Azure OpenAI's deployment-based endpoints. Exactly three things are new compared
with the OpenAI path this codebase has shipped for months: the URL shape, the
api-key header, and the absence of model β and all three are things we
emit, so all three are assertable.
It also checks the three ways an administrator might paste an endpoint (with or
without a trailing slash, with or without /openai) all normalise to the same
URL, and that a deployment name containing a space is escaped β an unencoded
space breaks the request line.
Since GH #171 it also covers:
a pasted Target URI or Foundry project address cut back to the resource; the
v1 route (no query string, deployment sent as model); a model that refuses
temperature: 0, and one (gpt5-like) that refuses both that and max_tokens
and must succeed in exactly three requests - including through tool calling;
#171's exact failure (a model version date as the API version answered with a
404, not retried, and explained as the API version); and a behavioural
control that an unrelated 400 (a content filter) surfaces after one attempt.
That control used to grep the source for the old retry line, which proved
nothing about behaviour and broke the moment the retry moved.
How it works
It starts its own php -S on a free port, serving only the
tests/azure-openai/ directory, and stops it again at the end. mock.php is a
stand-in Azure endpoint that records the request it received; the test then reads
that back and asserts on the bytes we sent. The real cURL path is exercised
rather than stubbed.
The mock is the one file in tests/ allowed to answer HTTP, and it accepts
only the built-in server β it refuses under Apache, nginx and php-fpm.
β οΈ On Windows
proc_open()must be givenbypass_shell, or the server is a grandchild ofcmd.exeandproc_terminate()leaves it running.
Run it
php tests/azure-openai/run.php
48 assertions. Needs nothing else running.
Reading the result
π΄ It ends with a warning, and the warning is the point:
Green here means WE SEND THE RIGHT REQUEST. It does not mean Azure accepts it β that needs a real tenant.
Nobody on the project has an Azure subscription to point it at. Content filtering, api-version drift, quota and regional behaviour are all outside what this can prove. Debug tool D014 exists for that gap: it makes one live call, prints the whole exchange and reads back Azure's answer (see AI Providers).
If it fails
"the mock server never came up" is an environment problem, not a code one β check nothing is blocking loopback sockets. Anything else means the request we build has changed shape.
tests/caldav-provider.php
What it tests
The CalDAV provider against a real server. The cases that matter:
- an edit keeps what the analyst added (a reminder) β the rule this provider exists to keep
- a stale change token gives a baseline, never "everything was deleted"
- a calendar address on another host is refused before any request β the per-analyst address must not become a way to reach anywhere
- the analyst's own appointments are never reported, only ours
Run it
It needs the Baikal fixture, seeded:
docker compose -f docker/carddav-test/docker-compose.yml up -d
bash docker/carddav-test/seed.sh
php tests/caldav-provider.php
Touches no database. Everything it writes is a FreeITSM-named event in the
throwaway itsm and tech2 calendars, and it deletes what it made.
Reading the result
If you see:
FAIL the address is a calendar server <- Failed to connect to localhost port 8092
Is the Baikal fixture running and seeded?
β¦the fixture is not up. That is an environment failure, not a code failure β start the fixture and run it again.
If it fails with the fixture running
The "stale token gives a baseline" case is the one with teeth: getting it wrong means a sync deletes a calendar's contents rather than resynchronising it.
tests/mailbox-folder-resolve.php
What it tests
mailboxResolveFolderId() β reading mail from a folder that is not the Inbox.
/mailFolders/<x>/messages does not take folder names. Graph accepts a short
list of well-known aliases there and treats everything else as an opaque folder
id, so a mailbox told to read freeitsm got:
400 ErrorInvalidIdMalformed β "Id is malformed."
INBOX had always worked purely because it is on the alias list. Reading a
folder by name had never worked for any name off it.
How it works
The resolver takes its HTTP fetcher as an argument, so the whole thing runs
against a fixture with no network and no mailbox. The fixture deliberately
contains a custom top-level folder and a same-named one inside the Inbox, so
freeitsm and Inbox/freeitsm must not resolve to the same place.
What it cannot prove is what Graph really returns.
Run it
php tests/mailbox-folder-resolve.php
15 assertions.
If it fails
A path resolving to the wrong folder means a mailbox silently collects from somewhere else β which looks to the operator like mail going missing.
tests/agent-empty-report-guard.php
What it tests
That an empty agent report does not empty the machine's inventory.
π΄ Both ingest endpoints wipe an asset's hardware tables and reinsert on every
report. The DELETE ran unconditionally while the INSERT was guarded by
!empty(...) β so a report that collected nothing for a category emptied that
category and left it empty. A WMI blip took 226 Device Manager rows with it,
with no error anywhere, until the next good run. Any client holding an API key
could do it deliberately with {"disks":{}}.
An empty list now means "I did not find out", not "there is nothing there": the wipe is skipped and the section is named in the response, because looking like a successful sync of nothing is the other half of the bug.
Run it
php tests/agent-empty-report-guard.php
ZZEG-prefixed, cleaned up including on failure.
If it fails
This one destroys customer data silently and is only noticed later. Treat any red here as blocking, and check both ingest endpoints β the bug was in both.