Skip to content

Feature/xtense architecture phase1#56

Merged
darknoon29 merged 7 commits intodevelopfrom
feature/xtense-architecture-phase1
Apr 17, 2026
Merged

Feature/xtense architecture phase1#56
darknoon29 merged 7 commits intodevelopfrom
feature/xtense-architecture-phase1

Conversation

@darknoon29
Copy link
Copy Markdown
Member

This pull request introduces a comprehensive setup for automated testing and improves error handling and code structure in the Xtense mod. The main changes include adding PHPUnit configuration and fixtures, introducing an abstract handler base class to reduce code duplication, refactoring error handling to use Monolog, and improving coordinate validation.

Testing infrastructure and fixtures:

  • Added composer.json with development dependencies (phpunit/phpunit, monolog/monolog) and a test script for Composer-based workflows.
  • Added a PHPUnit configuration file (phpunit.xml), a bootstrap file (tests/bootstrap.php), and multiple JSON fixtures for handler and message types to support unit testing. [1] [2] [3] [4] [5] [6] [7]

Core architecture improvements:

  • Introduced includes/AbstractHandler.php, a new abstract base class for all Xtense data type handlers, providing shared methods for grant checking, coordinate parsing, planet type resolution, upsert queries, callback registration, logging, and IO response handling. This reduces code duplication and centralizes common logic.

Error handling and logging:

  • Refactored error handling in includes/Io.php to use Monolog for logging callback errors, including exception context, instead of direct echo statements.

Validation and robustness:

  • Improved the Check::coords method in includes/Check.php to throw an InvalidArgumentException on invalid coordinates instead of terminating the process, making error handling more robust and testable.

…l error handler

- Check::coords() now throws InvalidArgumentException instead of die()
- Added try/catch around main switch+callbacks in xtense.php
- Exceptions now produce proper JSON error response with logging
- Previously, unhandled UnexpectedValueException would also crash raw
- Removed direct echo of error messages and stack traces to stdout
- Callback errors now logged via global \ (Monolog) with context
- Prevents raw text from corrupting JSON response to browser extension
- Also removes potential password leak (was echoing stacktrace without sanitization)
Provides shared methods to eliminate code duplication across the 18 case handlers:
- requireGrant(): grant check with IO error response (repeated 18x)
- parseCoordinates(): Check::coords() + explode + casting (repeated 12x)
- resolvePlanetType(): TYPE_PLANET/TYPE_MOON ternary (repeated 15x)
- planetTypeToString(): planet type to DB string
- executeUpsert(): INSERT...ON DUPLICATE KEY UPDATE builder (repeated 8x)
- registerCallback(): wraps CallbackHandler::add()
- logAction(): wraps add_log() with auto toolbar info
- setPageUpdatedResponse(): standard IO response pattern

No behavior changes - this is a foundation class for Phase 2 handler extraction.
Copilot AI review requested due to automatic review settings April 17, 2026 16:47
@darknoon29 darknoon29 merged commit 0d89bed into develop Apr 17, 2026
1 check passed
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR lays the groundwork for a phased “Xtense handler” architecture by adding a PHPUnit-based test harness + fixtures, introducing a shared AbstractHandler base class, and tightening error handling/logging + coordinate validation.

Changes:

  • Added Composer + PHPUnit setup (bootstrap + config) and a suite of unit tests with JSON fixtures (many handler tests are TDD placeholders that skip until handlers exist).
  • Introduced includes/AbstractHandler.php to centralize common handler logic (grant checks, coordinate parsing, upserts, callbacks, IO responses).
  • Refactored callback error reporting to log via Monolog and updated coordinate validation to throw on invalid format.

Reviewed changes

Copilot reviewed 53 out of 55 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
xtense.php Wraps main processing in a try/catch and fixes ranking match for empty type3.
includes/AbstractHandler.php New shared base class for future handler refactors.
includes/Io.php Switches callback error reporting from echo to Monolog logging.
includes/Check.php Changes invalid coordinate format handling to throw InvalidArgumentException.
composer.json Adds dev deps (PHPUnit, Monolog) and composer test script.
composer.lock Locks the added dev dependencies.
phpunit.xml PHPUnit configuration pointing at tests/bootstrap.php.
tests/bootstrap.php PHPUnit bootstrap that loads Composer, defines constants/stubs, loads xtense sources + optional handlers.
tests/unit/XtenseTestCase.php Base test case providing globals, stubs, constants, and helper methods.
tests/unit/SpyDatabase.php Capturing DB spy to assert on generated SQL.
tests/unit/SpyCallbackHandler.php Capturing callback handler spy for assertions.
tests/unit/AbstractHandlerTest.php Unit tests for AbstractHandler helpers via a concrete stub.
tests/unit/IoTest.php Unit tests for Io response building.
tests/unit/CheckTest.php Unit tests for Check validation helpers (notably coords()).
tests/unit/OverviewHandlerTest.php Skipped TDD tests for future OverviewHandler.
tests/unit/BuildingsHandlerTest.php Skipped TDD tests for future BuildingsHandler.
tests/unit/ResourceSettingsHandlerTest.php Skipped TDD tests for future ResourceSettingsHandler.
tests/unit/ResearchsHandlerTest.php Skipped TDD tests for future ResearchsHandler.
tests/unit/SystemHandlerTest.php Skipped TDD tests for future SystemHandler.
tests/unit/RankingHandlerTest.php Skipped TDD tests for future RankingHandler.
tests/unit/CombatReportHandlerTest.php Skipped TDD tests for future CombatReportHandler.
tests/fixtures/overview.json Fixture for overview payload tests.
tests/fixtures/buildings.json Fixture for buildings payload tests.
tests/fixtures/buildings_facilities.json Fixture for facilities buildings payload tests.
tests/fixtures/resourceSettings.json Fixture for resource settings payload tests.
tests/fixtures/researchs.json Fixture for research payload tests.
tests/fixtures/system.json Fixture for system payload tests.
tests/fixtures/defense.json Fixture for defense payload tests.
tests/fixtures/rc.json Fixture for basic combat report payload tests.
tests/fixtures/rc_complex.json Fixture for complex combat report payload tests.
tests/fixtures/messages_spy.json Fixture for spy message payload tests.
tests/fixtures/messages_spy_shared.json Placeholder fixture for shared spy message payload.
tests/fixtures/messages_rc_cdr.json Placeholder fixture for recycling report message payload.
tests/fixtures/messages_msg.json Placeholder fixture for generic message payload.
tests/fixtures/messages_expedition.json Placeholder fixture for expedition message payload.
tests/fixtures/messages_ennemy_spy.json Placeholder fixture for enemy spy message payload.
tests/fixtures/messages_ally_msg.json Placeholder fixture for ally message payload.
tests/fixtures/fleet.json Placeholder fixture for fleet payload.
tests/fixtures/ally_list.json Placeholder fixture for ally list payload.
tests/fixtures/ranking_player_points.json Fixture for player points ranking payload tests.
tests/fixtures/ranking_player_economy.json Fixture for player economy ranking payload tests.
tests/fixtures/ranking_player_fleet_built.json Fixture for player fleet-built ranking payload tests.
tests/fixtures/ranking_player_fleet_points.json Fixture for player fleet ranking payload tests.
tests/fixtures/ranking_player_fleet_loose.json Placeholder fixture for player fleet “loose” ranking payload.
tests/fixtures/ranking_player_fleet_honor.json Placeholder fixture for player fleet honor ranking payload.
tests/fixtures/ranking_player_fleet_destruct.json Placeholder fixture for player fleet destruct ranking payload.
tests/fixtures/ranking_player_research.json Placeholder fixture for player research ranking payload.
tests/fixtures/ranking_ally_points.json Placeholder fixture for ally points ranking payload.
tests/fixtures/ranking_ally_economy.json Placeholder fixture for ally economy ranking payload.
tests/fixtures/ranking_ally_fleet_built.json Placeholder fixture for ally fleet-built ranking payload.
tests/fixtures/ranking_ally_fleet_loose.json Placeholder fixture for ally fleet “loose” ranking payload.
tests/fixtures/ranking_ally_fleet_honor.json Placeholder fixture for ally fleet honor ranking payload.
tests/fixtures/ranking_ally_fleet_destruct.json Placeholder fixture for ally fleet destruct ranking payload.
tests/fixtures/ranking_ally_research.json Placeholder fixture for ally research ranking payload.
.gitignore Ignores vendor/ and PHPUnit cache.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +135 to +147
protected function executeUpsert(string $table, array $columns, array $values, array $updateColumns): void
{
$colStr = '`' . implode('`, `', $columns) . '`';
$valStr = implode(', ', array_map(function ($val) {
return is_int($val) || is_float($val) ? $val : "'" . $val . "'";
}, $values));
$updatePairs = array_map(function ($col) {
return "`$col` = VALUES(`$col`)";
}, $updateColumns);

$query = "INSERT INTO $table ($colStr) VALUES ($valStr) ON DUPLICATE KEY UPDATE " . implode(', ', $updatePairs);
$this->db->sql_query($query);
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

executeUpsert() interpolates values directly into SQL without escaping and doesn’t handle nulls (null becomes an empty quoted string and can emit notices). This is both a correctness and injection risk since many handler inputs originate from request payloads. Use the DB escape API for string values (and emit SQL NULL for nulls) rather than concatenating raw values.

Copilot uses AI. Check for mistakes.
Comment thread xtense.php
Comment on lines +1877 to +1881
} catch (\Throwable $e) {
$log->error("Xtense error: " . $e->getMessage(), ['exception' => $e]);
$io->set('error', $e->getMessage());
$io->status(0);
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The top-level catch sends the raw exception message back to the client in the JSON response. Some exception messages (e.g., DB/SQL errors, file paths) can leak internal details. Consider returning a generic error message to the client and keeping full details only in logs.

Copilot uses AI. Check for mistakes.
Comment thread includes/Io.php
Comment on lines 139 to +150
public function append_call_error($call, $message, ?Exception $e = null)
{
global $log;

$this->append_call($call, self::ERROR);
$this->append_call_message($message, self::ERROR, $call);


echo "* CALL ERROR ({$call['root']}):\n $message\n";

if ($e !== null) {
echo " Exception Stacktrace\n";
$stacktrace = str_replace("\n", "\n ", $e->getTraceAsString());
if (isset($db_password)) {
$stacktrace = str_replace($db_password, "*****", $stacktrace);
}
echo " " . $stacktrace;
}

echo "\n\n";

$context = ['mod' => $call['root'] ?? 'unknown'];
if ($e !== null) {
$context['exception'] = $e;
}
$log->error("Xtense callback error: $message", $context);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, append_call_error() echoed a stacktrace while scrubbing $db_password from the output. The new Monolog logging path drops that scrubbing and may log sensitive values contained in exception messages/traces depending on the formatter/handler. Consider sanitizing known secrets (or avoid attaching the exception object) before logging.

Copilot uses AI. Check for mistakes.
Comment thread includes/Check.php
Comment on lines 27 to 39
* Sanitize Coordinates string
* @param $string
* @param int $exp
* @return mixed
* @return string
* @throws \InvalidArgumentException
*/
static function coords($string, $exp = 0) {
global $server_config;
//if ($string == "unknown") return true; //cas avec une seule planète
if (!preg_match('!^([0-9]{1,2}):([0-9]{1,3}):([0-9]{1,2})$!Usi', $string, $match)) die("coords : Hack");
if (!preg_match('!^([0-9]{1,2}):([0-9]{1,3}):([0-9]{1,2})$!Usi', $string, $match)) {
throw new \InvalidArgumentException("Invalid coordinates format: " . substr((string)$string, 0, 30));
}
if (($match[1] < 1 || $match[2] < 1 || $match[3] < 1 || $match[1] > $server_config['num_of_galaxies'] || $match[2] > $server_config['num_of_systems'] || ($exp ? ($match[3] != 16) : ($match[3] > 15))) == false) return $string;
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check::coords() docblock says it returns string, but the function can return null for out-of-bounds coordinates (and now throws for invalid format). Update the return annotation/signature docs to reflect the actual behavior (e.g., ?string) so callers/tests are consistent.

Copilot uses AI. Check for mistakes.
Comment thread includes/Io.php
if ($e !== null) {
$context['exception'] = $e;
}
$log->error("Xtense callback error: $message", $context);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Io::append_call_error() now unconditionally calls $log->error() via a global, which will fatal if $log is unset or not a logger instance (e.g., some OGSpy contexts or early bootstrap failures). Consider guarding the call (isset/is_object/method_exists) and falling back to error_log() to preserve error reporting.

Suggested change
$log->error("Xtense callback error: $message", $context);
$logMessage = "Xtense callback error: $message";
if (isset($log) && is_object($log) && method_exists($log, 'error')) {
$log->error($logMessage, $context);
} else {
$fallbackMessage = $logMessage . ' [mod=' . $context['mod'] . ']';
if ($e !== null) {
$fallbackMessage .= ' [exception=' . $e->getMessage() . ']';
}
error_log($fallbackMessage);
}

Copilot uses AI. Check for mistakes.
Comment thread tests/bootstrap.php
] as $constName => $constValue) {
if (!defined($constName)) define($constName, $constValue);
}

Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests/bootstrap.php defines only a subset of TABLE_* constants, but XtenseTestCase::setUpBeforeClass() returns early when Io is already loaded, so its additional constants (e.g., TABLE_PARSEDRC / TABLE_PARSEDRCROUND / TABLE_ROUND_ATTACK / TABLE_ROUND_DEFENSE) won’t be defined when those handlers/tests are added. Either add the missing constants here or restructure XtenseTestCase bootstrap so required constants are always defined.

Suggested change
foreach ([
'TABLE_PARSEDRC' => $p . 'parsedrc',
'TABLE_PARSEDRCROUND' => $p . 'parsedrcround',
'TABLE_ROUND_ATTACK' => $p . 'round_attack',
'TABLE_ROUND_DEFENSE' => $p . 'round_defense',
] as $constName => $constValue) {
if (!defined($constName)) define($constName, $constValue);
}

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +68
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();

// Guard: files only need to be loaded once across all test classes.
if (class_exists('Io', false)) {
return;
}

Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XtenseTestCase::setUpBeforeClass() returns early when Io is already loaded, which also skips defining constants and loading support code that later tests/handlers may rely on. If bootstrap.php is the single source of loading now, consider removing this guard or ensuring any required constant definitions happen regardless of whether classes are already loaded.

Copilot uses AI. Check for mistakes.
Comment thread composer.json
Comment on lines +7 to +14
"phpunit/phpunit": "^13.0",
"monolog/monolog": "^3.9"
},
"scripts": {
"test": "vendor/bin/phpunit"
},
"config": {
"optimize-autoloader": true
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

composer.json requires phpunit/phpunit ^13, which (per composer.lock) requires PHP >= 8.4.1. If the project/mod is expected to be testable on earlier PHP versions (common for OGSpy deployments), consider constraining the PHP platform version and selecting a PHPUnit version compatible with it to avoid blocking composer install for contributors/CI.

Suggested change
"phpunit/phpunit": "^13.0",
"monolog/monolog": "^3.9"
},
"scripts": {
"test": "vendor/bin/phpunit"
},
"config": {
"optimize-autoloader": true
"phpunit/phpunit": "^10.5",
"monolog/monolog": "^3.9"
},
"scripts": {
"test": "vendor/bin/phpunit"
},
"config": {
"optimize-autoloader": true,
"platform": {
"php": "8.1.0"
}

Copilot uses AI. Check for mistakes.
Comment thread includes/Io.php
Comment on lines 139 to +150
public function append_call_error($call, $message, ?Exception $e = null)
{
global $log;

$this->append_call($call, self::ERROR);
$this->append_call_message($message, self::ERROR, $call);


echo "* CALL ERROR ({$call['root']}):\n $message\n";

if ($e !== null) {
echo " Exception Stacktrace\n";
$stacktrace = str_replace("\n", "\n ", $e->getTraceAsString());
if (isset($db_password)) {
$stacktrace = str_replace($db_password, "*****", $stacktrace);
}
echo " " . $stacktrace;
}

echo "\n\n";

$context = ['mod' => $call['root'] ?? 'unknown'];
if ($e !== null) {
$context['exception'] = $e;
}
$log->error("Xtense callback error: $message", $context);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

append_call_error() type-hints ?Exception, but callback execution can also fail with non-Exception Throwables (e.g., TypeError) which currently won’t be passable to this method if you want to log them. Using ?\Throwable instead will cover both Exceptions and Errors.

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +101
protected function parseCoordinates(string $coordString, int $exp = 0): array
{
$coords = Check::coords($coordString, $exp);
list($g, $s, $r) = explode(':', $coords);
return [
'coords' => $coords,
'galaxy' => (int)$g,
'system' => (int)$s,
'row' => (int)$r,
];
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AbstractHandler::parseCoordinates() assumes Check::coords() always returns a string, but Check::coords() returns null for out-of-bounds coordinates (and now throws only on format). If null is returned, explode()/list() will trigger warnings and the handler will proceed with invalid coords. Consider making parseCoordinates explicitly handle a null return (e.g., throw an exception / set IO error) before exploding.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants