props[$key] ?? false); } protected function ReadPropertyInteger(string $key): int { return (int) ($this->props[$key] ?? 0); } protected function ReadPropertyString(string $key): string { return (string) ($this->props[$key] ?? ''); } protected function ReadAttributeString(string $key): string { return (string) ($this->attrs[$key] ?? ''); } protected function ReadAttributeInteger(string $key): int { return (int) ($this->attrs[$key] ?? 0); } protected function GetValue(string $key) { return $this->values[$key] ?? false; } protected function UpdateFormField(string $name, string $field, $value): void { $this->fields[$name][$field] = $value; } } function IPS_GetInstance(int $id): array { return ['InstanceStatus' => $GLOBALS['instanceStatus']]; } require_once __DIR__ . '/../../Manager/module.php'; final class ManagerFormHarness extends Manager { public array $calls = []; public bool $reject = false; public function V4ManagerAktivtestSchalten(bool $aktiv): void { $this->calls[] = $aktiv; if ($this->reject) { throw new RuntimeException('Existing backend refused start'); } $this->values['NetzfahrplanV4Aktivtest'] = $aktiv; } } $count = 0; function check(bool $ok, string $name): void { global $count; if (!$ok) { throw new RuntimeException('FAIL ' . $name); } $count++; echo 'PASS ' . $name . PHP_EOL; } function fixture(): ManagerFormHarness { $h = new ManagerFormHarness(); $h->props = array_fill_keys(['PrognoseAktiv', 'NetzfahrplanV4SchattenAktiv', 'NetzfahrplanV4EmpfangAktiv', 'NetzfahrplanV4MessdatenAktiv', 'NetzfahrplanV4RegeltestErlaubt', 'NetzfahrplanV4AktivtestErlaubt', 'NetzfahrplanV4WatchdogVerzichtErlaubt'], true); $h->props['NetzfahrplanAktiv'] = false; // Synthetic fixture, not a license or credential from an installation. $h->props['Lizenzcode'] = 'ENX' . str_repeat('A', 16); $h->attrs['LizenzcodeHash'] = hash('sha256', $h->props['Lizenzcode']); $h->attrs['LizenzInstallationID'] = '10000000-0000-4000-8000-000000000001'; $h->attrs['LizenzLease'] = json_encode([ 'status' => 'active', 'development' => true, 'installationId' => $h->attrs['LizenzInstallationID'], 'issuedAt' => gmdate('c', time() - 100), 'refreshAfter' => gmdate('c', time() + 100), 'offlineUntil' => gmdate('c', time() + 3600), 'entitlements' => [ ['catalog_key' => 'manager_peak', 'quantity' => 1, 'status' => 'active'], ['catalog_key' => 'grid_schedule', 'quantity' => 1, 'status' => 'active'], ], ], JSON_THROW_ON_ERROR); $h->attrs['NetzfahrplanV4AktivtestEnde'] = time() + 600; $h->values = ['Aktiv' => true, 'NetzfahrplanV4Aktivtest' => false]; $GLOBALS['instanceStatus'] = 102; return $h; } function rejected(ManagerFormHarness $h, $value, string $name): void { try { $h->RequestAction('FormNetzfahrplanSchalten', $value); } catch (RuntimeException | InvalidArgumentException $e) { check($h->calls === [], $name); return; } check(false, $name); } $h = fixture(); $before = [$h->props, $h->attrs]; $h->RequestAction('FormNetzfahrplanSchalten', true); check($h->calls === [true], 'real manager action delegates exactly one explicit start'); check(!$h->fields['PrognoseNetzfahrplanStart']['enabled'], 'started session cannot be restarted from form'); check([$h->props, $h->attrs] === $before, 'form action changes no configuration, expiry or license'); $h->props['Lizenzcode'] = ''; $h->RequestAction('FormNetzfahrplanSchalten', false); check($h->calls === [true, false], 'stop still delegates after license loss'); check(!$h->values['NetzfahrplanV4Aktivtest'], 'stop status is refreshed'); foreach ([1, 'true', 'false', null, []] as $value) { rejected(fixture(), $value, 'nonboolean input rejected: ' . gettype($value)); } foreach (['PrognoseAktiv', 'NetzfahrplanV4SchattenAktiv', 'NetzfahrplanV4EmpfangAktiv', 'NetzfahrplanV4RegeltestErlaubt', 'NetzfahrplanV4AktivtestErlaubt', 'NetzfahrplanV4WatchdogVerzichtErlaubt'] as $prop) { $h = fixture(); $h->props[$prop] = false; rejected($h, true, 'server-side start guard checks ' . $prop); } $h = fixture(); $h->props['Lizenzcode'] = ''; rejected($h, true, 'missing license cannot be bypassed by calling the action directly'); $h = fixture(); $lease = json_decode($h->attrs['LizenzLease'], true); $lease['entitlements'][1]['quantity'] = 0; $h->attrs['LizenzLease'] = json_encode($lease); rejected($h, true, 'manager license alone does not grant grid schedule'); $h = fixture(); $h->props['NetzfahrplanAktiv'] = true; rejected($h, true, 'competing legacy controller blocks action'); $h = fixture(); $h->attrs['NetzfahrplanV4AktivtestEnde'] = time() - 1; rejected($h, true, 'expired consent cannot be renewed by the form'); $h = fixture(); $GLOBALS['instanceStatus'] = 203; rejected($h, true, 'inactive or faulty manager cannot start'); $h = fixture(); $h->values['Aktiv'] = false; rejected($h, true, 'disabled manager cannot start'); $h = fixture(); $h->values['NetzfahrplanV4Aktivtest'] = true; rejected($h, true, 'existing session cannot be restarted'); $h = fixture(); $h->reject = true; try { $h->RequestAction('FormNetzfahrplanSchalten', true); check(false, 'backend rejection preserved'); } catch (RuntimeException $e) { check($e->getMessage() === 'Existing backend refused start' && $h->calls === [true], 'backend rejection preserved without retry or bypass'); } $h = fixture(); $h->RequestAction('FormPrognoseAktiv', false); check(!$h->fields['PrognoseNetzfahrplanStart']['enabled'] && $h->calls === [], 'unsaved forecast disable prevents start without a control call'); $h = fixture(); $h->props['PrognoseAktiv'] = false; $h->RequestAction('FormPrognoseAktiv', true); check(!$h->fields['PrognoseNetzfahrplanStart']['enabled'] && $h->calls === [], 'unsaved forecast enable does not grant stored permission'); echo "Total: $count checks. Real RequestAction and license checks; mocked IPS and control delegate.\n";