*/ private array $scripts = []; /** @var array */ private array $recorded = []; /** @var array */ private array $files = []; /** @var array 3 is the private key, only for 'key' connections. */ public array $connections = []; public string $fingerprint = 'SHA256:fakehostkeyfingerprint'; /** When set, connect attempts throw (simulates an unreachable/rebooting host). */ public bool $failConnect = false; private CommandResult $default; public function __construct() { $this->default = CommandResult::success(); } public function script(string $substring, CommandResult|callable $result): static { $this->scripts[$substring] = $result; return $this; } public function defaultResult(CommandResult $result): static { $this->default = $result; return $this; } public function connectWithPassword(string $host, string $user, string $password): void { if ($this->failConnect) { throw new \RuntimeException("connection refused: {$host}"); } $this->connections[] = ['password', $host, $user]; } public function connectWithKey(string $host, string $user, string $privateKey, ?string $expectedFingerprint = null): void { if ($this->failConnect) { throw new \RuntimeException("connection refused: {$host}"); } $this->connections[] = ['key', $host, $user, $privateKey]; } public function run(string $command): CommandResult { $this->recorded[] = $command; foreach ($this->scripts as $substring => $result) { if (str_contains($command, $substring)) { return is_callable($result) ? $result($command) : $result; } } return $this->default; } /** * Observable through files(), NOT through ran(): an uploaded file is not a * command and is deliberately not recorded as one. Worth stating, because a * test once asserted `ran('/usr/local/sbin/clupilot-…-firewall.sh')` believing * it proved the script had been deployed — the only thing that substring * matched was the `chmod 700` on the line above, so the same fact was * asserted twice and the script's contents were never checked at all. */ public function putFile(string $remotePath, string $contents): void { $this->files[$remotePath] = $contents; } public function hostKeyFingerprint(): string { return $this->fingerprint; } // --- introspection helpers for tests --- /** @return array */ public function recorded(): array { return $this->recorded; } public function ran(string $substring): bool { foreach ($this->recorded as $command) { if (str_contains($command, $substring)) { return true; } } return false; } /** @return array */ public function files(): array { return $this->files; } /** @return array */ public function connectionsWith(string $type): array { return array_values(array_filter($this->connections, fn ($c) => $c[0] === $type)); } }