Files
cacti/tests/Unit/Database/DataInputDataTemplateCopyTest.php
Sean Mancini 96033267cb refactor(tests): reorganize unit tests into domain category folders (#7733)
* refactor(tests): reorganize unit tests into domain category folders

- Consolidate per-advisory GHSA regression tests into Security/ subfolders by vuln class
- Group all 186 test files into domain folders: DataCollection/, Ui/, Core/,
  Database/, Installer/, Scripts/, Security/, Plugin/
- Strip GitHub issue/GHSA numbers from filenames; keep IDs in test descriptions
- Fix all __DIR__ relative paths to match new folder nesting depth
- Update CI workflow path triggers and phpunit XML configs for new file locations
- Update .github/copilot-instructions.md with unit test organization guidelines
- Add tests/vendor/ to .gitignore

* add gaurds

* fix up guard for unit tests

* fix redeclaration of cacti_log in tests

* fix updates to unit tests

* fix test patjhs

* fix: update test __DIR__ paths for reorganized test structure

* fix: conditionally define read_user_setting to prevent redeclaration in tests

* fix: conditionally define functions to prevent redeclaration in tests

- Wrap clean_up_lines, raise_message, cacti_debug_backtrace, and get_debug_prefix
  with function_exists() checks to prevent conflicts when test stubs are loaded
  before lib/functions.php in the same PHP process

* fix: add unit test bootstrap to prevent function redeclaration errors

- Create tests/bootstrap-unit.php to load core libraries before tests
- Ensures lib/functions.php loads before test stubs define stub functions
- Test stubs use function_exists() checks, so they detect pre-loaded functions
- Update phpunit.xml to use bootstrap-unit.php for Unit testsuite
- Fixes 'Cannot redeclare read_user_setting()' errors when tests run in same process

* fix: load test stubs before global.php in unit test bootstrap

- Reorder bootstrap to load CactiStubs.php and UnitStubs.php first
- Ensures stub functions like __() are available when global.php loads
- global.php calls get_cacti_version_text() which needs __() for translations
- Fixes 'Call to undefined function __()' error during bootstrap

* fix: remove function conflicts from test stubs

- Remove stub definitions for functions already in lib/functions.php:
  cacti_sizeof, cacti_log, cacti_strtolower, read_config_option,
  read_user_setting, cacti_count
- Keep translation function stubs (__(), __esc()) needed by global.php
- Update bootstrap to load lib/functions.php after stubs
- This prevents function redeclaration errors while maintaining test isolation

* fix: prevent constant and variable redeclaration in bootstrap

- Remove explicit PHP_TESTING and CACTI_TEST_BOOTSTRAP defines from bootstrap
  (CactiStubs.php now handles this with guards)
- Add guards to CactiStubs.php to prevent redefinition of PHP_TESTING
- Only set CACTI_TEST_BOOTSTRAP env vars if not already set
- Fixes PHP warnings about 'Constant X already defined'

* fix: remove redundant lib/functions.php load from bootstrap

include/global.php already loads lib/functions.php at lines 287-288.
The bootstrap was trying to load it again after global.php, but global.php
needs format_cacti_version() to be available on lines 293-294 to define
CACTI_VERSION and CACTI_VERSION_FULL constants.

Solution: Let global.php handle loading lib/functions.php itself, and don't
load it separately in the bootstrap.

* fix: remove more conflicting function stubs from test helpers

- Remove stubs for functions also defined in lib/functions.php:
  raise_message, clean_up_lines, cacti_debug_backtrace, get_debug_prefix
- Remove POLLER_ID constant stub (defined unconditionally by global.php)
- Keep only translation functions and test-specific helpers
- This prevents function/constant redeclaration errors

* fix: minimize test stubs to only translation functions

Remove ALL helper functions and constants from UnitStubs.php that are:
- Defined in lib/functions.php (raise_message, clean_up_lines, etc.)
- Defined in lib/html_utility.php (srv, set_request_var)
- Defined in global_constants.php (POLLER_VERBOSITY_*, MESSAGE_LEVEL_*)
- Defined in global.php (POLLER_ID)

Keep ONLY translation functions (__(), __esc()) that are:
- Called by global.php during initialization
- Not defined elsewhere
- Required for proper bootstrap sequence

This eliminates all constant and function redeclaration errors.

* fix: remove __esc stub - already defined in global_languages.php

Keep only __() translation function stub, which is needed by global.php
and not defined elsewhere in Cacti core.

* fix: handle no-arg case in __() translation stub

The __() function may be called with just a string and no format arguments.
Previously, vsprintf() would fail with 'arguments array must contain 2 items'.
Now, if no args are provided, return the text as-is.
2026-08-16 19:23:31 -07:00

149 lines
6.9 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
/*
* Source-level guards for the data_input_data column-expansion bugs in
* change_data_template() (lib/template.php) and api_data_source_duplicate()
* (lib/api_data_source.php).
*
* Background. The data_input_data table on develop carries five identity
* columns (data_input_field_id, data_template_data_id, data_template_id,
* local_data_id, host_id) plus the value pair (t_value, value). Two
* write paths copy rows from the template (parent) row into the child:
*
* - change_data_template() in lib/template.php
* - api_data_source_duplicate() in lib/api_data_source.php
*
* Two regressions were live in the develop branch:
*
* (1) change_data_template() copied $item['data_template_id'],
* $item['local_data_id'], $item['host_id'] from the parent row
* verbatim. For template rows those are 0, so child data_input_data
* rows ended up with local_data_id = 0 and host_id = 0, orphaned
* from the new data source. The fix reads host_id from
* data_local for the new $local_data_id and stamps the child row
* with the function's own ($data_template_id, $local_data_id,
* $host_id) instead of the parent's.
*
* (2) api_data_source_duplicate() expanded the INSERT column list to
* seven (data_input_field_id, data_template_data_id,
* data_template_id, local_data_id, host_id, t_value, value) but
* left the placeholder list at four (?, ?, ?, ?), while still
* binding seven values. db_execute_prepared() rejects the
* mismatch, so duplicating a data source or template silently
* produced no data_input_data rows.
*
* Both bugs cascade into the same downstream symptom TheWitness reported
* for 1.2.31: data sources created from a template (or duplicated from
* an existing one) end up without populated data_input_data, which
* breaks update_poller_cache() and leaves poller_item empty.
*
* The 1.2.x branch is unaffected. Its data_input_data schema only
* carries the four columns (data_input_field_id, data_template_data_id,
* t_value, value) and both call sites already match.
*/
$templateSource = file_get_contents(__DIR__ . '/../../../lib/template.php');
$apiDataSourceSource = file_get_contents(__DIR__ . '/../../../lib/api_data_source.php');
/**
* Extract the body of one PHP function definition out of a source blob.
* Caller supplies the function name; returns the matched body or '' if
* the definition is not found.
*/
function _extract_function_body(string $source, string $name): string {
$pattern = '/^function\s+' . preg_quote($name, '/') . '\b[^{]*\{.*?^\}/sm';
if (!preg_match($pattern, $source, $m)) {
return '';
}
return $m[0];
}
test('change_data_template stamps child data_input_data rows with the new identity tuple', function () use ($templateSource) {
$body = _extract_function_body($templateSource, 'change_data_template');
expect($body)->not->toBe('');
/* The function must look up the child host_id from data_local using
* the new $local_data_id. The pre-fix code did not do this and
* relied on $item['host_id'] from the parent row, which is 0 on a
* template. */
expect($body)
->toContain('SELECT host_id')
->toContain('FROM data_local')
->toContain('[$local_data_id]');
/* The REPLACE INTO must use the function's own identity tuple
* ($data_template_id, $local_data_id, $host_id) for the three
* identity columns that previously came from the parent row. */
$replacePos = strpos($body, 'REPLACE INTO data_input_data');
expect($replacePos)->not->toBeFalse('REPLACE INTO data_input_data must remain present');
$insertSlice = substr($body, $replacePos, 800);
/* Column count and placeholder count must match. */
expect(substr_count($insertSlice, '?'))
->toBe(7);
/* The seven bound values must include the function's own variables,
* not just $item[...] readbacks of the parent row. */
expect($insertSlice)
->toContain('$data_template_id,')
->toContain('$local_data_id,')
->toContain('$host_id,');
/* The pre-fix bind list pulled these from $item; that pattern must
* not survive in the fixed code for the identity columns. */
expect(strpos($insertSlice, "\$item['local_data_id']"))->toBeFalse(
'must not bind $item[local_data_id] for the child row (template default is 0)'
);
expect(strpos($insertSlice, "\$item['host_id']"))->toBeFalse(
'must not bind $item[host_id] for the child row (template default is 0)'
);
});
test('api_data_source_duplicate INSERT column count matches placeholder count', function () use ($apiDataSourceSource) {
$body = _extract_function_body($apiDataSourceSource, 'api_data_source_duplicate');
expect($body)->not->toBe('');
$insertPos = strpos($body, 'INSERT IGNORE INTO data_input_data');
expect($insertPos)->not->toBeFalse('INSERT IGNORE INTO data_input_data must remain present');
$insertSlice = substr($body, $insertPos, 800);
/* Placeholder count must match the seven-column form the caller
* already binds. The pre-fix code had four placeholders against
* seven bound values; db_execute_prepared() rejects that. */
expect(substr_count($insertSlice, '?'))
->toBe(7);
/* All seven columns must be named in the INSERT list. */
foreach (['data_input_field_id', 'data_template_data_id', 'data_template_id', 'local_data_id', 'host_id', 't_value', 'value'] as $col) {
expect($insertSlice)->toContain($col);
}
});
test('1.2.x schema regression check: develop INSERT must not silently drop columns', function () use ($apiDataSourceSource, $templateSource) {
/* If anyone re-narrows the develop INSERT/REPLACE column lists back
* to the 1.2.x four-column form (data_input_field_id,
* data_template_data_id, t_value, value) the column / placeholder
* count check above will still pass on length but the cross-source
* grammar drifts. Pin the seven-column shape explicitly so a
* narrowing change has to delete the columns from the SQL string
* rather than just re-shrink the bind array. */
foreach ([$templateSource, $apiDataSourceSource] as $source) {
$pos = strpos($source, '(data_input_field_id, data_template_data_id, data_template_id, local_data_id, host_id, t_value, value)');
expect($pos)->not->toBeFalse('seven-column data_input_data write must remain present');
}
});