Bugfix Issue 659 AddTestCaseToTestPlan UndefExternalId - sebiboga/testlink-upgraded GitHub Wiki

Issue 659 โ€” addTestCaseToTestPlan(): undefined $tcase_external_id at xmlrpc.class.php:3522

Issue: #659 Branch: fix/issue-659 ยท Commit: 98fac3e3b (fix, +2 lines) Status: FIXED & VERIFIED (2026-08-24)

Symptom

Calling tl.addTestCaseToTestPlan with a valid test case but a nonexistent version produced a degraded error message and an E_WARNING row in the Event Viewer on every occurrence:

(addTestCaseToTestPlan) - Version (99) does not exist for Test Case (:issue659_tc).
E_WARNING
Undefined variable $tcase_external_id - .../lib/api/xmlrpc/v1/xmlrpc.class.php - Line 3522

Measured during reproduction (fresh DB): fixtures created via the XMLRPC API (project id=1 prefix I659, plan id=2, suite id=3, case id=4 = I659-1 version 1), then a single call with version: 99 โ†’ events id=2, log_level=2, message above. The %s slot meant to carry the external id rendered as an empty string, leaving a dangling colon before the case name.

Root cause chain

  1. addTestCaseToTestPlan() opens at xmlrpc.class.php:3462; its local init block (:3467-3473) never defines $tcase_external_id.
  2. The version-existence block (:3513-3524) rejects unknown versions at :3520 and builds its message at :3522 with sprintf(TCASE_VERSION_NUMBER_KO_STR, $version_number, $tcase_external_id, ...) โ€” reading the variable that was never assigned anywhere in this method.
  3. Locale format (locale/en_GB/strings.txt:3207) "Version (%s) does not exist for Test Case (%s:%s)." โ†’ PHP coerces the undefined variable to NULL โ†’ empty slot โ†’ (:name); under PHP 8.3 logging each occurrence also writes an E_WARNING to events.

Whole-file grep confirmed $tcase_external_id = exists exactly once outside this bug site โ€” inside checkTestCaseAncestry() (:3824), a different method. Legacy 1.9.20 code predating strict PHP; same family as #457's audit findings.

Blast radius

Exactly one faulty call site (:3522). No other screen or API method shares the line. Sibling patterns available:

  • checkTestCaseAncestry() :3823-3824 โ€” derives the id via $this->tcaseMgr->getExternalID($tcase_id) โœ” established pattern;
  • :5904 uses $this->args[self::$testCaseExternalIDParamName] โœ˜ not safe here: callers may pass testcaseid instead of the external id.

Approach โ€” derive the id from the case id (minimal)

Chosen fix (+2 lines inserted after old :3521; they land as post-fix lines :3522-3523, immediately before the sprintf):

$dummy = $this->tcaseMgr->getExternalID( $tcase_id, $tproject_id );
$tcase_external_id = isset( $dummy[0] ) ? $dummy[0] : '';

Why this way:

  • Mirrors the in-file sibling pattern (checkTestCaseAncestry()), so the code stays consistent with how the same variable is computed elsewhere.
  • Passes the already-in-scope $tproject_id (assigned at :3486 before this block, and validated by checkTestCaseAncestry() just above): supplying it lets getExternalID()'s static prefix/root cache (lib/functions/testcase.class.php:5699-5707) resolve without the redundant tree-walk query that getPrefix($id, null) performs when no project is given, and keeps same-project repeat calls on correct cache hits.
  • The isset guard covers getExternalID() returning [] when the case has no last version info (testcase.class.php:5714-5715).

Rejected alternatives:

  • Using $this->args[self::$testCaseExternalIDParamName] like :5904 โ€” breaks for testcaseid callers;
  • Suppressing warnings globally โ€” hides future defects;
  • Refactoring the check block โ€” larger diff with zero behavioral gain.

Verification

# Check Result
R1 version=99 post-fix fault 5051, message (I659-1:issue659_tc), no new E_WARNING
R2 version=1 valid link success; testplan_tcversions gains the link row
R3 cross-project case O659-1 vs plan of project 1 ancestry rejection unchanged (7007, correct ids)
โ€” Event Viewer watermark across all runs zero new log_level=2 rows
โ€” php -l xmlrpc.class.php clean

Regression suite: tmp/TLU_Test_Cases.md โ†’ "Suite 659 โ€” 6/6 PASS".