Files
api/services/nginx/app/tests/Smoke/boolean_normalization_smoke.php
T
Jeppe Bandopenhands a71194d46e refactor(api): centralise truthy-string -> bool coercion in a shared trait (#368)
## What

Centralises the truthy-string → bool coercion that six different classes
were reimplementing (and which two implementations disagreed about).

The shared helper lives in
`services/nginx/app/traits/boolean_normalization_t.php`:

```php
namespace traits;
trait boolean_normalization_t {
    public static function normalizeBoolean(mixed $value): bool {
        if (is_bool($value)) return $value;
        return in_array(strtolower(trim((string)$value)), ['1','true','yes','on'], true);
    }
}
```

`traits/module_config_variable_t::inputToBool` now delegates to it. The
seven call sites that previously inlined the same expression (or wrapped
it in a private `toBool`/`boolValue`/`isEnabled`) are reduced to a
single `self::normalizeBoolean(...)` call:

| Class | Old helper | New |
| --- | --- | --- |
| `classes/cron_worker.php` | inline in `boolOption` |
`self::normalizeBoolean(...)` (after empty-value short-circuit) |
| `classes/replica_failover_manager.php` | `boolValue` |
`self::normalizeBoolean(...)` |
| `classes/release_manager.php` | `toBool` |
`self::normalizeBoolean(...)` |
| `classes/releasemanager.php` | `isEnabled` |
`self::normalizeBoolean(...)` |
| `classes/superuser_system_status_service.php` | inline in
`parseModuleConfigValue` | `self::normalizeBoolean(...)` |
| `classes/module_usage_service.php` | `toBool` |
`self::normalizeBoolean(...)` |
| `classes/account_deletion_service.php` | inline in `apiEnabled` |
`self::normalizeBoolean(...)` |
| `traits/module_config_variable_t.php` | `inputToBool` (narrow set) |
`self::normalizeBoolean(...)` (full set) |

## Why

The pre-PR repo had two silent bugs:

1. **`inputToBool` accepted only `'true'`/`'1'`** while the six inline
copies accepted the wider `['1','true','yes','on']` set. Config values
such as `"yes"` or `" ON "` would round-trip to `false` through
`inputToBool` but `true` through any of the inline copies. This PR picks
the wider set as the single source of truth; the change is a strict
superset, so no caller flips from truthy to falsy.
2. **Six copies of the same expression** to drift in any of the seven
places (whitespace handling, case sensitivity, empty-string semantics).
One trait replaces them.

## Tests

* `tests/Unit/Traits/BooleanNormalizationTest.php` — Pest, runs the
helper directly through anonymous-class composition (no DB/HTTP).
* `tests/Smoke/boolean_normalization_smoke.php` — standalone PHP smoke
runner for environments without composer installed. Verified locally:
7/7 consumer wiring checks pass, 19/19 normalization cases pass (`true`,
`false`, `1`, `0`, `'true'`, `'TRUE'`, `'1'`, `'yes'`, `'YES'`, `'on'`,
`' ON '`, `'false'`, `'no'`, `'off'`, `''`, `null`, `'0'`, `[]`,
stdClass).
* All eight touched files pass `php -l` syntax check.

## Risk

* Behavioural change is a strict superset for the shared expression
path, so no caller can flip from truthy → falsy. The only consumer that
saw a behaviour change for *negative* inputs is `inputToBool` itself,
which previously rejected `'yes'`/`'on'`. Worth a CI pass on the
unit/integration suites before merge.

## Co-author

Co-authored-by: openhands <openhands@all-hands.dev>

---

_This PR was generated by an AI agent (OpenHands) on behalf of
copenhagentruckwash._

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-08-12 20:20:03 +02:00

70 lines
2.1 KiB
PHP

<?php
/**
* Standalone smoke test for the boolean_normalization_t trait.
*
* The composer autoloader is not always available locally (CI may install
* dependencies before this script runs); the inline require_once calls
* below let us verify the trait + all seven consumers in isolation.
*/
declare(strict_types=1);
require_once __DIR__ . '/../../traits/boolean_normalization_t.php';
require_once __DIR__ . '/../../classes/cron_worker.php';
require_once __DIR__ . '/../../classes/replica_failover_manager.php';
require_once __DIR__ . '/../../classes/superuser_system_status_service.php';
require_once __DIR__ . '/../../classes/module_usage_service.php';
require_once __DIR__ . '/../../classes/account_deletion_service.php';
require_once __DIR__ . '/../../classes/releasemanager.php';
require_once __DIR__ . '/../../classes/release_manager.php';
$classes = [
'classes\\cron_worker',
'classes\\replica_failover_manager',
'classes\\superuser_system_status_service',
'classes\\module_usage_service',
'classes\\account_deletion_service',
'classes\\releasemanager',
'classes\\release_manager',
];
foreach ($classes as $class) {
$rc = new ReflectionClass($class);
$ok = in_array('traits\\boolean_normalization_t', $rc->getTraitNames(), true);
echo str_pad($class, 55) . ' -> ' . ($ok ? 'YES' : 'NO') . PHP_EOL;
}
echo PHP_EOL;
$cases = [
[true, true],
[false, false],
[1, true],
[0, false],
['true', true],
['TRUE', true],
['1', true],
['yes', true],
['YES', true],
['on', true],
[' ON ', true],
['false', false],
['no', false],
['off', false],
['', false],
[null, false],
['0', false],
[[], false],
[(object) ['v' => 'true'], false],
];
$s = new class {
use traits\boolean_normalization_t;
};
$fails = 0;
foreach ($cases as $pair) {
[$in, $exp] = $pair;
$a = $s::normalizeBoolean($in);
if ($a !== $exp) {
echo 'FAIL ' . var_export($in, true) . ' expected ' . var_export($exp, true) . ' got ' . var_export($a, true) . PHP_EOL;
$fails++;
}
}
echo ($fails === 0 ? 'OK' : 'FAIL') . ' - ' . count($cases) . ' normalizeBoolean cases' . PHP_EOL;