Priority Bar Chart Modernized - sebiboga/testlink-upgraded GitHub Wiki

Priority Bar Chart β€” Modernized Screen

Modernization of the Priority Bar Chart report (lib/results/priorityBarChart.php) β€” GitHub issue #1845; the three defects found while building and reviewing it are #1846, #1847 and #1848.

With every ASIDE entry already pointing at a modern screen, this report was picked by the sweep as the smallest coherent legacy slice that still had no modern twin β€” and it turned out to be an orphan that could not run at all on 2.0.1. It is now a standalone Dashio page (gui/templates/results/priorityBarChart.html) backed by a plain-PHP REST BFF (api/prioritybarchart/index.php), reachable from the Results by Keyword section of the modern Graphical Charts report.

Path: Results β†’ Graphical Charts β†’ Results by Keyword β†’ Open Priority Bar Chart URL: gui/templates/results/priorityBarChart.html?tplan_id=N&tproject_id=P BFF API: api/prioritybarchart/index.php (GET|HEAD ?action=init&tplan_id=N[&tproject_id=P]) Legacy URL: lib/results/priorityBarChart.php β€” now a launcher shim (302 / 405 / 400) Auth: session (401 anonymous) + same-origin CSRF guard + the testplan_metrics right on the owning test project.

1. What was wrong with it

Legacy statement Reality in 2.0.1
require_once("../../third_party/charts/charts.php") file gone β€” phpchart dropped with third_party/charts/
require_once("../functions/results.class.php") file gone β€” split into per-area classes
results::getAggregateKeywordResults() method gone with the class above

Any request β†’ uncaught Error β†’ HTTP 500 with an empty body. Measured before the fix:

curl -s -o /dev/null -w '%{http_code}\n' -b <cookie> \
  'http://localhost:8082/lib/results/priorityBarChart.php?tplan_id=1&tproject_id=1'
# 500

On top of that it was an IDOR: testlinkInitPage() validates the session only, tplan_id came raw out of $_REQUEST and hasRight() was never called, so any authenticated account could read the per-keyword result breakdown of any test plan in the installation. tproject_id was read from the session and never compared with the plan's real owner, so the project "assertion" was decorative.

2. What the screen does now

  • Context strip β€” test project, test plan, assigned versions, platforms, keywords (3 in project vs 2 plotted, so an unused keyword is visible).
  • One stacked horizontal bar per keyword β€” passed (teal) / failed (red) / blocked (dark) / not executed (grey) with the progress percentage on the right, 1 din 2 executate (50%) style.
  • DataTable β€” the same numbers plus the raw result count and a bold Totals row.
  • Export CSV β€” ; separated, UTF-8 BOM, one line per keyword + a Totals line, named priority-bar-chart-<plan>.csv.
  • Refresh and Close (pops the report, like every other modern report).
  • State cards for bad request / session expiry / no right / plan not found / project mismatch / server error β€” each localized, each keeping the raw machine code visible so a support request is self-explanatory.
  • Empty state β€” "no keyword of this test project is attached to a test case of this test plan" instead of an empty chart box.

3. The rebuilt aggregate (documented decision)

tcversions has no testcase_id column in 2.0.1, the plan↔version link is a plain testplan_tcversions row (assign_tcversion_to_plan() no longer exists) and builds hang off builds.testproject_id. The bar is built like this:

  1. universe = distinct tcversion_id in testplan_tcversions for the plan;
  2. each version is attributed the result of its latest execution row (MAX(executions.id) for plan + version) β€” a version passed then failed is counted once, as failed, and the four buckets always partition the keyword total exactly;
  3. a version with no execution row falls into not executed;
  4. the raw number of execution rows per keyword is its own column, so work done in several builds stays visible;
  5. progress % = executed versions / versions in the keyword.

Measured on the fixture: login = 4 versions / 2 failed / 1 blocked / 1 not run / 4 results / 3 executed / 75 %; checkout = 2 versions / 1 passed / 1 not run / 50 %; totals 6 versions, 5 results, 4 executed, 66.7 %.

4. Contract

Case Answer
?action=init&tplan_id=N 200 {status:"ok", context, keywords[], totals}
no / non-numeric / array / negative tplan_id, unknown action 400 invalid_request
no session 401 not_authenticated (or session_expired)
no testplan_metrics on the owning project 403 no_right + tLog audit row
unknown plan 404 tplan_not_found
tproject_id β‰  owning project 404 project_mismatch
POST with same-origin Origin 405 method_not_allowed
POST without same-origin proof 403 CSRF (shared bffEnforceSession helper)
HEAD 200 (safe verb)

Every response carries Cache-Control: no-store + X-Content-Type-Options: nosniff, and every parameter is a bound placeholder, never interpolated.

5. The legacy URL now redirects instead of fataling

lib/results/priorityBarChart.php keeps testlinkInitPage() β€” so an anonymous caller still lands on login.php?note=expired, the 1.9.20 contract β€” and then:

  • 302 for a browser navigation β†’ priorityBarChart.html?tplan_id=…&tproject_id=… (tproject_id defaults to the session project when absent),
  • 405 modern_endpoint_only for an XHR caller (the legacy answer was a PNG β€” there is nothing left to serve),
  • 400 invalid_request JSON when tplan_id is missing.

No ASIDE row, template or JS caller of the legacy file is left; the only two mentions left in the repo are explanatory comments.

6. Wiring

  • $actions->priorityBarChart in lib/functions/common.php, inside the $tplan_id > 0 guard, so the report is plan-scoped like every other plan report;
  • a visible Open Priority Bar Chart button on the Results by Keyword section of gui/templates/results/charts.html β€” the report family that shares this aggregate:

7. i18n

46 pbc.* keys + footers.priorityBarChart + charts.openPriorityBarChart in all 10 client bundles (en ro de fr es it pt ja ru zh), validated with python3 -m json.tool. Verified in the browser by switching the locale: header, subtitle, legend, the progress text, all eight table headers and the footer translate and no raw dotted key ever reaches the DOM.

8. Bugs found while building and reviewing it

# Defect Symptom Fix
#1846 fetchFirstRowSingleColumn() called with 1 argument instead of 2 in the first BFF commit every request answered HTTP 500 with an empty body get_recordset() + (int) $rs[0]['n']
#1847 bar() composed its label key from the CSS class β†’ pbc.seriesNot_run, a key that exists in no bundle the grey segment's tooltip showed the raw dotted key (TLi18n.t() has no fallback β€” the #1840 class); invisible in the a11y snapshot the four series are declared in one SERIES map
#1848 the unknown-failure branch fed the server message to TLi18n.t(), leaving pbc.serverErrorBody dead a 500 printed an English server string inside a localized card localized sentence + escaped raw detail + http_<status> fallback, {status} interpolated

The mandatory code review added 6 findings on top: the order-of-operations comment contradicted the code and the tproject_id assertion ran before the rights check (an IDOR probe β€” now the api/tcsummary order), platform_id = 0 counted as a real platform, a redundant GROUP BY/COUNT(*) in the latest-result query, a duplicated version map, and unquoted CSV cells. All fixed.

9. Tests

Suite 1845 in tmp/TLU_Test_Cases.md β€” 71/71 PASS (python3 tmp/suite_1845.py, exit 0) plus 9 browser cases:

  • auth: admin 200, anonymous 401, a real role-3 login 403 (also with a forged Origin), the refused right audited in the Event Viewer;
  • the aggregate semantics in both directions (latest-wins, exact partition, progress %, raw result count);
  • project isolation proven with a second project + plan β€” each plan reads its own keywords, a foreign tproject_id is refused;
  • the full 400/401/403/404/405 matrix, HEAD, no-store/nosniff;
  • all four legacy-shim branches;
  • wiring, and all 10 bundles.

Event Viewer: no new Warning/Error row β€” the only log_level=1 entries are the deliberate no_right audit rows, same convention as api/namecheck/checkDuplicateName.php.

Fixture: tmp/fixtures_1845.php β€” project PBC1 + plan PBC Plan (keywords checkout/login + one unused), plan PBC Empty Plan with no assigned version, project PBC2 + plan PBD Plan holding foreign-keyword, user pbcnorights (role 3).

Docs mirror: docs/Priority-Bar-Chart.md.

⚠️ **GitHub.com Fallback** ⚠️