Skip to content

Commit 20af5bb

Browse files
committed
[TEST] Add tests to reach 99.5% coverage target #2
1 parent ddbdb5d commit 20af5bb

7 files changed

Lines changed: 1033 additions & 20 deletions

src/Client/SocketConnection.php

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ class SocketConnection
2121
*/
2222
public static function send(string $socketPath, array $payload, float $timeout = 30.0): array
2323
{
24-
if (!file_exists($socketPath)) {
25-
throw new DaemonException("Socket not found: {$socketPath}. Is the daemon running?");
26-
}
24+
self::throwDaemonExceptionIf(
25+
!file_exists($socketPath),
26+
"Socket not found: {$socketPath}. Is the daemon running?",
27+
);
2728

2829
$socket = @stream_socket_client(
2930
"unix://{$socketPath}",
@@ -32,9 +33,10 @@ public static function send(string $socketPath, array $payload, float $timeout =
3233
$timeout,
3334
);
3435

35-
if ($socket === false) {
36-
throw new DaemonException("Cannot connect to daemon: [{$errorCode}] {$errorMessage}");
37-
}
36+
self::throwDaemonExceptionIf(
37+
$socket === false,
38+
"Cannot connect to daemon: [{$errorCode}] {$errorMessage}",
39+
);
3840

3941
stream_set_timeout($socket, (int)$timeout, (int)(($timeout - (int)$timeout) * 1_000_000));
4042

@@ -62,20 +64,30 @@ public static function send(string $socketPath, array $payload, float $timeout =
6264
$meta = stream_get_meta_data($socket);
6365
fclose($socket);
6466

65-
if ($meta['timed_out']) {
66-
throw new DaemonException('Daemon request timed out');
67-
}
67+
self::throwDaemonExceptionIf($meta['timed_out'], 'Daemon request timed out');
6868

6969
$response = trim($response);
70-
if ($response === '') {
71-
throw new DaemonException('Empty response from daemon');
72-
}
70+
71+
self::throwDaemonExceptionIf($response === '', 'Empty response from daemon');
7372

7473
$decoded = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
75-
if (!is_array($decoded)) {
76-
throw new DaemonException('Invalid response from daemon');
77-
}
74+
75+
self::throwDaemonExceptionIf(!is_array($decoded), 'Invalid response from daemon');
7876

7977
return $decoded;
8078
}
79+
80+
/**
81+
* @param bool $condition
82+
* @param string $message
83+
* @return void
84+
*
85+
* @throws DaemonException
86+
*/
87+
private static function throwDaemonExceptionIf(bool $condition, string $message): void
88+
{
89+
if ($condition) {
90+
throw new DaemonException($message);
91+
}
92+
}
8193
}

src/Logging/StreamLogger.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,7 @@ public function __construct(mixed $target = 'php://stderr', string $minLevel = L
4545
if (!is_dir($dir)) {
4646
mkdir($dir, 0755, true);
4747
}
48-
$stream = fopen($target, 'a');
49-
if ($stream === false) {
50-
throw new \RuntimeException("Cannot open log file: {$target}");
51-
}
52-
$this->stream = $stream;
48+
$this->stream = self::openOrFail($target);
5349
$this->ownsStream = true;
5450
}
5551
}
@@ -92,6 +88,19 @@ private function interpolate(string $message, array $context): string
9288
return strtr($message, $replacements);
9389
}
9490

91+
/**
92+
* @param string $path
93+
* @return resource
94+
*/
95+
private static function openOrFail(string $path)
96+
{
97+
$stream = @fopen($path, 'a');
98+
if ($stream === false) {
99+
throw new \RuntimeException("Cannot open log file: {$path}");
100+
}
101+
return $stream;
102+
}
103+
95104
public function __destruct()
96105
{
97106
if ($this->ownsStream && is_resource($this->stream)) {
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Gianfriaur\OpcuaPhpClient\Client;
6+
use Gianfriaur\OpcuaPhpClient\Exception\ConnectionException;
7+
use Gianfriaur\OpcuaPhpClient\Types\DataValue;
8+
use Gianfriaur\OpcuaPhpClient\Types\BuiltinType;
9+
use Gianfriaur\OpcuaPhpClient\Types\Variant;
10+
use Gianfriaur\OpcuaSessionManager\Daemon\CommandHandler;
11+
use Gianfriaur\OpcuaSessionManager\Daemon\Session;
12+
use Gianfriaur\OpcuaSessionManager\Daemon\SessionStore;
13+
14+
describe('CommandHandler Extended', function () {
15+
16+
beforeEach(function () {
17+
$this->store = new SessionStore();
18+
$this->handler = new CommandHandler($this->store);
19+
});
20+
21+
describe('handleClose', function () {
22+
23+
it('closes an existing session', function () {
24+
$client = $this->createStub(Client::class);
25+
$session = new Session('s1', $client, 'opc.tcp://localhost:4840', [], microtime(true));
26+
$this->store->create($session);
27+
28+
$result = $this->handler->handle(['command' => 'close', 'sessionId' => 's1']);
29+
30+
expect($result['success'])->toBeTrue();
31+
expect($result['data'])->toBeNull();
32+
expect($this->store->count())->toBe(0);
33+
});
34+
35+
it('closes session even when disconnect throws', function () {
36+
$client = $this->createStub(Client::class);
37+
$client->method('disconnect')->willThrowException(new RuntimeException('disconnect failed'));
38+
$session = new Session('s1', $client, 'opc.tcp://localhost:4840', [], microtime(true));
39+
$this->store->create($session);
40+
41+
$result = $this->handler->handle(['command' => 'close', 'sessionId' => 's1']);
42+
43+
expect($result['success'])->toBeTrue();
44+
expect($this->store->count())->toBe(0);
45+
});
46+
47+
it('returns session_not_found for non-existent session', function () {
48+
$result = $this->handler->handle(['command' => 'close', 'sessionId' => 'nonexistent']);
49+
50+
expect($result['success'])->toBeFalse();
51+
expect($result['error']['type'])->toBe('session_not_found');
52+
});
53+
54+
});
55+
56+
describe('handleOpen error paths', function () {
57+
58+
it('returns error on connect failure', function () {
59+
$result = $this->handler->handle([
60+
'command' => 'open',
61+
'endpointUrl' => 'opc.tcp://nonexistent-host:99999',
62+
'config' => [],
63+
]);
64+
65+
expect($result['success'])->toBeFalse();
66+
});
67+
68+
it('applies opcuaTimeout config', function () {
69+
$result = $this->handler->handle([
70+
'command' => 'open',
71+
'endpointUrl' => 'opc.tcp://nonexistent-host:99999',
72+
'config' => ['opcuaTimeout' => 0.1],
73+
]);
74+
75+
expect($result['success'])->toBeFalse();
76+
});
77+
78+
it('applies autoRetry config', function () {
79+
$result = $this->handler->handle([
80+
'command' => 'open',
81+
'endpointUrl' => 'opc.tcp://nonexistent-host:99999',
82+
'config' => ['autoRetry' => 0, 'opcuaTimeout' => 0.1],
83+
]);
84+
85+
expect($result['success'])->toBeFalse();
86+
});
87+
88+
it('applies batchSize config', function () {
89+
$result = $this->handler->handle([
90+
'command' => 'open',
91+
'endpointUrl' => 'opc.tcp://nonexistent-host:99999',
92+
'config' => ['batchSize' => 50, 'opcuaTimeout' => 0.1],
93+
]);
94+
95+
expect($result['success'])->toBeFalse();
96+
});
97+
98+
it('applies defaultBrowseMaxDepth config', function () {
99+
$result = $this->handler->handle([
100+
'command' => 'open',
101+
'endpointUrl' => 'opc.tcp://nonexistent-host:99999',
102+
'config' => ['defaultBrowseMaxDepth' => 20, 'opcuaTimeout' => 0.1],
103+
]);
104+
105+
expect($result['success'])->toBeFalse();
106+
});
107+
108+
it('applies securityPolicy and securityMode config', function () {
109+
$result = $this->handler->handle([
110+
'command' => 'open',
111+
'endpointUrl' => 'opc.tcp://nonexistent-host:99999',
112+
'config' => [
113+
'securityPolicy' => 'http://opcfoundation.org/UA/SecurityPolicy#None',
114+
'securityMode' => 1,
115+
'opcuaTimeout' => 0.1,
116+
],
117+
]);
118+
119+
expect($result['success'])->toBeFalse();
120+
});
121+
122+
it('applies username/password config', function () {
123+
$result = $this->handler->handle([
124+
'command' => 'open',
125+
'endpointUrl' => 'opc.tcp://nonexistent-host:99999',
126+
'config' => [
127+
'username' => 'admin',
128+
'password' => 'secret',
129+
'opcuaTimeout' => 0.1,
130+
],
131+
]);
132+
133+
expect($result['success'])->toBeFalse();
134+
});
135+
136+
it('applies clientCache when configured', function () {
137+
$cache = $this->createStub(\Psr\SimpleCache\CacheInterface::class);
138+
$handler = new CommandHandler($this->store, clientCache: $cache);
139+
140+
$result = $handler->handle([
141+
'command' => 'open',
142+
'endpointUrl' => 'opc.tcp://nonexistent-host:99999',
143+
'config' => ['opcuaTimeout' => 0.1],
144+
]);
145+
146+
expect($result['success'])->toBeFalse();
147+
});
148+
149+
});
150+
151+
describe('Error sanitization', function () {
152+
153+
it('sanitizes error messages from generic Throwable', function () {
154+
$client = $this->createStub(Client::class);
155+
$client->method('read')->willThrowException(
156+
new RuntimeException('Error at /home/user/secret/path.php: failed')
157+
);
158+
$session = new Session('s1', $client, 'opc.tcp://localhost:4840', [], microtime(true));
159+
$this->store->create($session);
160+
161+
$result = $this->handler->handle([
162+
'command' => 'query', 'sessionId' => 's1', 'method' => 'read',
163+
'params' => [['ns' => 0, 'id' => 2259, 'type' => 'numeric'], 13],
164+
]);
165+
166+
expect($result['success'])->toBeFalse();
167+
expect($result['error']['message'])->toContain('[path]');
168+
expect($result['error']['message'])->not->toContain('/home/user/secret');
169+
});
170+
171+
});
172+
173+
describe('Certificate validation in handleOpen', function () {
174+
175+
it('rejects userCertPath that does not exist', function () {
176+
$result = $this->handler->handle([
177+
'command' => 'open',
178+
'endpointUrl' => 'opc.tcp://localhost:4840',
179+
'config' => [
180+
'userCertPath' => '/nonexistent/user-cert.pem',
181+
'userKeyPath' => '/nonexistent/user-key.pem',
182+
],
183+
]);
184+
185+
expect($result['success'])->toBeFalse();
186+
expect($result['error']['message'])->toContain('does not exist');
187+
});
188+
189+
it('validates allowedCertDirs path resolution failure', function () {
190+
$tmpFile = tempnam(sys_get_temp_dir(), 'opcua_test_');
191+
file_put_contents($tmpFile, 'fake cert');
192+
193+
try {
194+
$handler = new CommandHandler($this->store, allowedCertDirs: ['/nonexistent/allowed']);
195+
196+
$result = $handler->handle([
197+
'command' => 'open',
198+
'endpointUrl' => 'opc.tcp://localhost:4840',
199+
'config' => [
200+
'clientCertPath' => $tmpFile,
201+
'clientKeyPath' => $tmpFile,
202+
],
203+
]);
204+
205+
expect($result['success'])->toBeFalse();
206+
expect($result['error']['message'])->toContain('not in an allowed directory');
207+
} finally {
208+
unlink($tmpFile);
209+
}
210+
});
211+
212+
});
213+
214+
});

0 commit comments

Comments
 (0)