Reorder Test Cases Modernized - sebiboga/testlink-upgraded GitHub Wiki

Modernize: Reorder Test Cases (tcReorder)

Refs #1660 ยท status: DONE ยท BFF: api/tcreorder/index.php ยท screen: gui/templates/testcases/tcReorder.html

What this screen is

Re-ordering the test cases inside a test suite. In 1.9.20 it was not a screen at all โ€” it was the drag-and-drop of the ExtJS tree that formed the left frame of the Test Specification work area:

Legacy piece Role
lib/testcases/listTestCases.php the left frame, rendering tcTree.tpl
gui/templates/dashio/testcases/tcTree.tpl the ExtJS tree widget
gui/javascript/execTree.js โ†’ writeNodePositionToDB() drag-drop client
lib/ajax/dragdroptprojectnodes.php the write backend
containerEdit.php (reorderTestCasesDictionary / reorderTestCasesByExtID) the sort action

Because it was a tree gesture, the capability had no addressable URL, no dialog, and no right of its own. It is now an ordinary screen.

Why the legacy backend had to be retired

lib/ajax/dragdroptprojectnodes.php performed no authorization at all:

  • doAction=changeParent called tree::change_parent() on an arbitrary node id
  • doAction=doReorder called tree::change_order_bulk() on an arbitrary comma-separated list of node ids

There was no rights check and no test-project ownership check, so any authenticated user โ€” including one holding no test-case management right whatsoever โ€” could re-parent nodes across test projects (and touch requirement and test-plan nodes) or rewrite arbitrary node_order values.

The modern BFF closes that: it checks mgt_modify_tc on the owning project and proves every submitted node id to be a test case of the addressed container before writing.

The screen

Dashio layout: teal header + dark toolbar (Refresh / Back to Test Specification / Close), a context card (Test project, Container, Reorder by, Test cases), a container card with the suite selector and the two sort buttons, the order table, and explicit loading / empty / access-denied / not-found states (a 401 bounces to /login.php, as on every other modernized screen). TLi18n locale switcher in the toolbar.

Control Behaviour
Container selector project root + every suite of the project, alphabetical
Move to top / up / down / bottom per row, immediate (no confirm), boundary buttons disabled
Sort by name confirmation naming the criterion, then natsort order
Sort by external ID same, numeric-aware order

Rows show position, name, external id and the four move buttons. Boundary state is recomputed from the server response after every write, never guessed client-side.

BFF contract

api/tcreorder/index.php

Route Notes
GET ?action=init&tproject_id=N[&container_id=M] context, container, suites, criterion, ordered test cases, rights
POST ?action=move node_id, position = top|bottom|up|down
POST ?action=sort by = NAME|EXTERNALID
POST ?action=reorder nodelist โ€” the hardened drag-and-drop parity endpoint

Guards: session auth, bffSameOriginGuard(), bffEnforceSession() (the legacy inactivity timeout โ€” this endpoint writes), and mgt_modify_tc on the owning project. Status contract: 401 anonymous / 403 no rights / 403 container from another project / 400 bad param / 404 unknown project, container or node / 405 wrong verb. Every write answers no_change instead of ok when the id order did not actually move, so the screen never toasts "saved" for a no-op.

The 2.0.1 tree schema โ€” the trap for every modernized screen

The first version of this BFF was written against 1.9.20 and every read was dead SQL. 2.0.1's nodes_hierarchy carries only id, name, parent_id, node_type_id, node_order; the columns testproject_id, testcase_id, tcversion_id and testsuite_id are gone:

Unknown column 'nh.testproject_id' in 'SELECT'

The real relationships:

  • a test case node id is the test case id
  • a version node (node_type_id = 4) has parent_id = the test case node, and tcversions.id = the version node id
  • external id = MAX(tc_external_id) over those version rows
  • suite metadata is in testsuites(id, details)
  • the project name lives in nodes_hierarchy.name; testprojects keeps only prefix
  • ownership must be proved by walking parent_id up to node_type_id = 1 โ€” there is no column to read

There is also no table-prefix global in 2.0.1; interpolating one raises E_WARNING Undefined global variable on every query. Use tlObjectWithDB::getDBTables(), which is also the only accessor that honours a configured prefix.

Sorting parity

config_get('testcase_reorder_by') ships as the string EXTERNAL_ID (config.inc.php), not EXTERNALID. Legacy dispatches on it in containerEdit.php:

  • NAME โ†’ reorderTestCasesDictionary() โ†’ natsort(strtolower(name))
  • otherwise โ†’ reorderTestCasesByExtID() โ†’ ORDER BY tc_external_id

So the criterion is normalized with NAME as the opt-in. A plain strcasecmp would put TC 10 before TC 2; a natural compare fixes that. External ids compare numerically when both are numeric โ€” legacy sorted the VARCHAR column as text, and '2' before '10' is never what a user means. Non-numeric ext ids (TC-A) fall back to a natural text compare.

Retired legacy surface

Both controllers are now thin shims, and neither replays a mutation:

Legacy URL Anonymous Authenticated
lib/ajax/dragdroptprojectnodes.php โ†’ login (destination preserved) GET โ†’ 302 to this screen; POST โ†’ 405
lib/testcases/listTestCases.php โ†’ login (destination preserved) 302 โ†’ edit_tc / keywordsAssign / assignReqs; unknown feature โ†’ 400

Wiring

  • $actions->tcReorder in lib/functions/common.php
  • rights-gated Reorder Test Cases button + openTcReorder() on gui/templates/testcases/testSpec.html (hidden without mgt_modify_tc)

i18n

tcreo.* + footers.tcReorder + tspec.reorderTestCases / tspec.reorderTestCasesTitle - 46 keys - in all 10 bundles (python3 -m json.tool validated). The bundles are FLAT (dotted keys at top level), not nested objects. Dead keys were removed rather than left behind: tcreo.confirmReorder (its only caller doReorderAll() was unreachable), tcreo.reorderSaved, and tcreo.stSession / tcreo.stSessionMsg once the 401 path became a redirect.

Bugs found and fixed while building this

Issue Defect
#1661 2.0.1 dropped four nodes_hierarchy columns โ€” every read was dead SQL. Plus: the default container always 404'd, the suite selector was always empty (the tree walk omitted parent_id, which is exactly what the ownership proof starts from), a no-op write reported ok, and a container from another project was silently retargeted and reordered.
#1662 Both shims bounced anonymous users to a relative login.php, which resolved against /lib/ajax/ and /lib/testcases/ โ†’ 404. The session-expiry path landed on a dead page.
#1663 E_WARNING Undefined global variable $dbprefix on every query โ€” 1 289 event rows, which buried the two log lines that actually mattered.
final smoke pass The in-flight guard re-enabled every move button from its own .always(), undoing the boundary state of the response it had just rendered (the first and last row offered a no-op move again), its setBusy(true) counterpart was never called so there was no guard at all, and a 401 rendered a session card with no way out of it - now a bounce to /login.php.
code review Inverted sort criterion (EXTERNAL_ID vs EXTERNALID, so the screen silently sorted by name on every install), no inactivity-timeout check on a write endpoint, strcasecmp where legacy used natsort, plus 5 MINOR and 7 NIT items.

Testing

Suite 1660 in tmp/TLU_Test_Cases.md โ€” 62 cases, all PASS, across rendering/data contract, move, sort (incl. modal cancel/apply), container switching and empty states, errors and rights, legacy shim retirement, i18n and the Event Viewer. Fixture tmp/fixtures_1660.php (project A 69 with a root suite of 5, a nested suite, an empty suite and a test-plan node; project B 95; a no rights and a view-only user). Event Viewer clean โ€” zero new rows after the getDBTables fix.

Screenshots

The screen with a populated container โ€” the container selector, the context card and the boundary-aware move buttons:

Reorder Test Cases

The sort confirmation, naming the criterion that will be applied to every test case of the container:

Sort confirmation

After Move to bottom on the third row โ€” the order is re-read from the server and the boundary buttons are recomputed:

After a move

The empty state for a container with no test cases โ€” the table is hidden, the count reads 0 and the sort buttons are disabled:

Empty container

A user holding only mgt_view_tc โ€” the toolbar button is hidden and a direct URL lands on this card:

Access denied

Romanian locale:

Romanian locale

Commits

f7944810b BFF โ†’ 0dd2b1754 screen + i18n โ†’ e11a7a335 wiring + shims โ†’ d927d4306 schema rebuild (#1661) โ†’ a8062fd4a shim redirect (#1662) โ†’ 22254e0ae getDBTables (#1663) โ†’ 5190c6b67 suite โ†’ e3b4ec6f8 code-review fixes.