Developer Tests System - edmozley/freeitsm GitHub Wiki
๐งช Developer Tests โ System & infrastructure
Part of Developer Tests. Database verification, config.php,
containers, clocks and the status portal.
๐ Every test on this page exists because the fault was invisible on a
developer's machine. A UTC bug does not show on a server whose clock is already
UTC. An open_basedir bug does not show where open_basedir is unset. A
config.php bug does not show on the one install whose config.php is the
repository's. So these tests deliberately do not ask "does it work here" โ
they ask the structural question, or they recreate the hostile condition on
purpose.
| Test | Needs |
|---|---|
db-verify-indexes/run.php |
Nothing โ this is the one CI runs |
config-not-load-bearing.php |
Nothing |
container-detection.php |
Nothing |
utc-connection.php |
Database (temporary table only) |
service-status-portal.php |
Database |
cost-centres.php |
Database (rolled back) |
cost-centre-assign.php |
Database (rolled back) |
system-settings-search/run.php |
Nothing |
analyst-lifecycle.php |
Database |
tests/db-verify-indexes/run.php
What it tests
That the index backfill list understands FULLTEXT.
database/freeitsm.sql builds a new install; Database Verification upgrades
an existing one. Indexes bridge the two through a generated mirror
(includes/db_verify_indexes.php), so a grown install can be given an index it
never received. That mirror used to record a single boolean per index โ unique or
not โ which had nowhere to put a third kind.
Full-text search needs that third kind, and getting it wrong fails quietly:
- Type loss. A
FULLTEXT KEYparsed as an ordinaryKEYstill produces the right number of indexes, so a count check passes while every full-text search silently returns nothing on upgraded installs. - The truthy trap. The old third element was a bool read as
$unique ? 'UNIQUE KEY' : 'KEY'. The new one is a string โ and the string'key'is truthy, so any code path still using that ternary would build a UNIQUE index over columns full of duplicates. - Drift. The mirror is generated, so the generator and the drift self-check must agree about what an index "is". They share one parser precisely so they cannot disagree, and this suite pins that.
Run it
php tests/db-verify-indexes/run.php
32 assertions, needs nothing.
๐ This is the only test currently wired into CI, via schema-drift.yml,
which also runs php scripts/gen_db_verify_indexes.php --check.
If it fails
The CI job will have failed too. A drift failure means the generated mirror and
freeitsm.sql disagree โ regenerate the mirror rather than hand-editing it, and
work out which of the two files is actually wrong before you do.
tests/config-not-load-bearing.php
What it tests
That config.php is not load-bearing.
๐ด A change once put dbConnectionOptions() into config.php and pointed eleven
callers at it. But config.php is the operator's file: it ships as a template
carrying a credentials path, so every install edits it once and keeps that copy,
and the Docker image copies docker/config.php straight over the top. Upgrading
therefore delivered the callers and left the definition behind โ HTTP 500,
empty body, every page in the product.
๐ The rule: config.php is for values the operator chooses. Behaviour lives in
includes/, which upgrades with the product.
How it works
โ ๏ธ Why the ordinary suite could not catch it: the development machine's
config.php is the repository's config.php. On the one install where that file
is not customised, the function was present and everything worked.
So this test never asks "does it work here". It asks the structural question:
does config.php declare any functions at all?
Run it
php tests/config-not-load-bearing.php
13 assertions, needs nothing.
If it fails
Something executable has been added to config.php. Move it into includes/ and
have config.php call it. Remember docker/config.php is a different file with
different contents โ it defines DB_PASSWORD and other things a hand install must
never have โ so do not try to reconcile the two.
tests/container-detection.php
What it tests
storagePersistenceInContainer(), which gates the warning that tells an operator
that docker compose up -d --build is about to destroy their uploads,
unrecoverably. If it wrongly answers "no", the warning silently stops appearing
for the only people it was ever written for.
๐ด The failure mode is invisible on a development machine. It only appears when
open_basedir is set, which no developer here has, and its symptom is not an
error but silence. That is exactly how the reported bug reached a user.
How it works
It drives PHP as a subprocess with the restriction switched on, rather than testing the function in-process โ the condition cannot be created any other way.
โ ๏ธ It also guards against the wrong fix. The reported warning goes away if you
simply re-point the check at a path open_basedir allows โ and that tests clean
on any machine not running Docker, because there the right answer is false
anyway. Case 5 fails if anybody does that.
Run it
php tests/container-detection.php
12 assertions. Reads only: no database, no writes, nothing outside the repo.
If it fails
If case 5 is the red one, read the warning above before "fixing" it โ you are probably about to make the test pass and the feature stop working.
tests/utc-connection.php
What it tests
The database connection's clock.
MySQL evaluates NOW(), CURRENT_TIMESTAMP and CURDATE() in the connection's
session time zone. That defaulted to SYSTEM โ the database server's own
clock โ while FreeITSM stores every instant in UTC. So any INSERT that did not
name a datetime column wrote a local wall clock through the column's
DEFAULT CURRENT_TIMESTAMP, and the screen read it back as a UTC instant. A note
came out the server's own offset into the future.
๐ด The fault is invisible on a server whose clock is already UTC, which is most of them and every sensible container. A suite that only ever runs on such a machine agrees with the bug.
So the assertions do not ask "is the value right". They ask "is the connection's clock UTC", and then prove the consequence on a scratch table.
Run it
php tests/utc-connection.php
Safe: writes only to a TEMPORARY table, which exists for this connection only
and disappears when the script ends. Nothing in the real schema is touched.
If it fails
The connection is not being set to UTC. Every DEFAULT CURRENT_TIMESTAMP on the
install is now writing local time into a column everything else reads as UTC โ
and on a UTC server you will not see it. Fix the connection setup, not the
columns.
tests/service-status-portal.php
What it tests
Internal versus external incident updates. The point of this file is that an internal update never reaches the portal. Everything else is secondary, so each check is written from the side that must be refused as well as the side that must work.
How it works
โ ๏ธ It puts the portal settings back exactly as it found them. They are global,
and a test that leaves the portal publishing would be worse than no test.
ZZSS-named rows, removed including on failure.
Run it
php tests/service-status-portal.php
If it fails
An internal update reaching the portal is a disclosure to every customer at once โ internal updates are where engineers write frankly about what broke. Treat as blocking.
If the run dies part-way, check the portal settings by hand before doing anything else; the restore may not have run.
tests/cost-centres.php
3.1.0, #160. CostCentresService: codes kept as text and unique per company ignoring case, a parent in the same company and never a loop, delete refused with children, company scope (404 outside), and sync() - matched on code, all or nothing with every error listed, dry_run writing nothing, deactivate_missing. 38 checks inside one transaction that is always rolled back. See Cost centres โ Developer Guide.
php tests/cost-centres.php
tests/cost-centre-assign.php
3.5.0, #160 stage 2. Charging assets, contracts and people to a cost centre (includes/cost_centre_assign.php), 30 checks, rolled back:
- only active cost centres of the record's own company (none means the Default company);
- an inactive one already held is kept;
- moving company needs the new company's cost centre or none;
- the asset import matches by code, never treating digits as an id;
- the contracts API's code field and the old free text;
- delete is refused while anything is charged.
See Cost centres โ Developer Guide.
php tests/cost-centre-assign.php
tests/system-settings-search/run.php
3.5.0. The System landing search finds settings inside cards (systemAreaSettings() in system/includes/areas.php). For every listed setting, the heading id must exist on the card's page and its label key must exist in lang/en; every card heading must be listed. It needs nothing, and fails when a heading is renamed or added without updating the list.
php tests/system-settings-search/run.php
tests/analyst-lifecycle.php
What it tests
When an analyst leaves - Deactivate, Anonymise, Delete (#178, 3.7.0). See When an analyst leaves.
- The map is complete. Every column that refers to an analyst - found three ways: a foreign key to
analystsinfreeitsm.sql, a person-shaped integer column name, and aprincipal/member/owner/targettype + id pair - must be inanalystReferenceRegistry(), and every map entry must be a real column. This is the guard: a new table that points at analysts fails here until somebody decides what leaving does to it. - The actions do what they say, on throwaway analysts (
zz-test-lc-*):- an analyst with only personal settings deletes cleanly, dashboard widgets and preferences with them (the #178 case);
- an analyst with one ticket note cannot be deleted, and nothing changes;
- open work blocks Delete; a ticket is unassigned through the ticket service (its history records it) and an article moves to another analyst; moving work to an inactive analyst is refused;
- Deactivate keeps the name; Anonymise replaces the name, username and email, removes sign-in links, keeps their note credited to them, replaces the audit's copy of the name - and leaves a text that only contains it, and another analyst's name, alone;
- you cannot do any of it to yourself.
How it works
The throwaway tickets are written straight into the table with no priority and no requester, so creating them fires no ticket.created workflow and no email; work is moved to nobody for the ticket (an assignment can email the new assignee) and to a person only for the article. Every row - analysts, tickets, notes, audit rows, articles and the system_logs lines - is removed by id in a finally and checked gone.
Run it
php tests/analyst-lifecycle.php
If it fails
"every column that refers to an analyst is in the map" names the column. Add it to analystReferenceRegistry() in includes/analyst_lifecycle.php with the kind that fits - personal, secret, live, setup or history - or, if it does not refer to an analyst at all, to the test's $notAnalysts list with the reason.