Browse Source

Merge pull request #40 from EvilFreelancer/development

Update to version 1.3
tags/1.3.0 1.3.0
Paul Zloi 6 years ago
committed by GitHub
parent
commit
2f822d8be6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .travis.yml
  2. 165
      README.md
  3. 15
      composer.json
  4. 44
      configs/routeros-api.php
  5. 2
      examples/bridge_hosts.php
  6. 6
      examples/different_queries.php
  7. 30
      examples/export.php
  8. 2
      examples/interface_print.php
  9. 2
      examples/ip_address_print.php
  10. 2
      examples/ip_filrewall_address-list_print.php
  11. 2
      examples/queue_simple_print.php
  12. 2
      examples/queue_simple_print_v2.php
  13. 4
      examples/queue_simple_write.php
  14. 14
      examples/remove_security_profile.php
  15. 2
      examples/system_package_print.php
  16. 6
      examples/vlans_bridge.php
  17. 6
      examples/vlans_bridge_v2.php
  18. 6
      examples/vlans_bridge_v3.php
  19. 14
      phpunit.xml
  20. 2
      preconf.tcl
  21. 15
      src/APIConnector.php
  22. 6
      src/APILengthCoDec.php
  23. 168
      src/Client.php
  24. 106
      src/Config.php
  25. 2
      src/Helpers/ArrayHelper.php
  26. 77
      src/Interfaces/ClientInterface.php
  27. 34
      src/Interfaces/ConfigInterface.php
  28. 2
      src/Interfaces/QueryInterface.php
  29. 2
      src/Interfaces/StreamInterface.php
  30. 24
      src/Laravel/ClientWrapper.php
  31. 6
      src/Laravel/Facade.php
  32. 4
      src/Laravel/ServiceProvider.php
  33. 63
      src/Laravel/Wrapper.php
  34. 10
      src/Query.php
  35. 27
      src/ResponseIterator.php
  36. 73
      src/ShortsTrait.php
  37. 23
      src/SocketTrait.php
  38. 27
      src/Streams/ResourceStream.php
  39. 22
      src/Streams/StringStream.php
  40. 8
      tests/APIConnectorTest.php
  41. 25
      tests/APILengthCoDecTest.php
  42. 224
      tests/ClientTest.php
  43. 55
      tests/ConfigTest.php
  44. 4
      tests/Helpers/ArrayHelperTest.php
  45. 5
      tests/Helpers/BinaryStringHelperTest.php
  46. 2
      tests/Helpers/TypeHelperTest.php
  47. 62
      tests/Laravel/ServiceProviderTests.php
  48. 35
      tests/Laravel/TestCase.php
  49. 30
      tests/QueryTest.php
  50. 39
      tests/ResponseIteratorTest.php
  51. 46
      tests/Streams/ResourceStreamTest.php
  52. 29
      tests/Streams/StringStreamTest.php

2
.travis.yml

@ -27,7 +27,7 @@ before_script:
- ./preconf.tcl 12223 > /dev/null || true - ./preconf.tcl 12223 > /dev/null || true
- ./preconf.tcl 22223 > /dev/null || true - ./preconf.tcl 22223 > /dev/null || true
- composer self-update - composer self-update
- composer install --prefer-source --no-interaction --dev
- composer install --no-interaction --dev
script: script:
- vendor/bin/phpunit --coverage-clover=coverage.clover - vendor/bin/phpunit --coverage-clover=coverage.clover

165
README.md

@ -6,7 +6,7 @@
[![Code Coverage](https://scrutinizer-ci.com/g/EvilFreelancer/routeros-api-php/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/EvilFreelancer/routeros-api-php/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/EvilFreelancer/routeros-api-php/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/EvilFreelancer/routeros-api-php/?branch=master)
[![Scrutinizer CQ](https://scrutinizer-ci.com/g/evilfreelancer/routeros-api-php/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/evilfreelancer/routeros-api-php/) [![Scrutinizer CQ](https://scrutinizer-ci.com/g/evilfreelancer/routeros-api-php/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/evilfreelancer/routeros-api-php/)
# RouterOS PHP7 API Client
# RouterOS API Client
composer require evilfreelancer/routeros-api-php composer require evilfreelancer/routeros-api-php
@ -17,6 +17,11 @@ to work with PHP7 in accordance with the PSR standards.
You can use this library with pre-6.43 and post-6.43 versions of You can use this library with pre-6.43 and post-6.43 versions of
RouterOS firmware, it will be detected automatically on connection stage. RouterOS firmware, it will be detected automatically on connection stage.
## Minimum requirements
* `php` >= 7.2
* `ext-sockets`
## Laravel framework support ## Laravel framework support
RouterOS API client is optimized for usage as normal Laravel package, all functional is available via `\RouterOS` facade, RouterOS API client is optimized for usage as normal Laravel package, all functional is available via `\RouterOS` facade,
@ -32,10 +37,10 @@ $config = new \RouterOS\Config([
$client = new \RouterOS\Client($config); $client = new \RouterOS\Client($config);
``` ```
Call facade and pass array of parameters to `getClient` method:
Call facade and pass array of parameters to `client` method:
```php ```php
$client = \RouterOS::getClient([
$client = \RouterOS::client([
'host' => '192.168.1.3', 'host' => '192.168.1.3',
'user' => 'admin', 'user' => 'admin',
'pass' => 'admin', 'pass' => 'admin',
@ -43,26 +48,37 @@ $client = \RouterOS::getClient([
]); ]);
``` ```
### Laravel installation
You also may get array with all configs which was obtained from `routeros-api.php` file:
Install the package via Composer:
```php
$config = \RouterOS::config([
'host' => '192.168.1.3',
'user' => 'admin',
'pass' => 'admin',
'port' => 8728,
]);
composer require evilfreelancer/routeros-api-php
dump($config);
$client = \RouterOS::client($config);
```
### Laravel installation
By default the package will automatically register its service provider, but
if you are a happy owner of Laravel version less than 5.3, then in a project, which is using your package
By default, the package will automatically register its service provider, but
if you are a happy owner of Laravel version less than 5.5, then in a project, which is using your package
(after composer require is done, of course), add into`providers` block of your `config/app.php`: (after composer require is done, of course), add into`providers` block of your `config/app.php`:
```php ```php
'providers' => [ 'providers' => [
// ... // ...
RouterOS\Laravel\ClientServiceProvider::class,
RouterOS\Laravel\ServiceProvider::class,
], ],
``` ```
Optionally, publish the configuration file if you want to change any defaults: Optionally, publish the configuration file if you want to change any defaults:
php artisan vendor:publish --provider="RouterOS\\Laravel\\ClientServiceProvider"
php artisan vendor:publish --provider="RouterOS\\Laravel\\ServiceProvider"
## How to use ## How to use
@ -88,16 +104,51 @@ $query =
$response = $client->query($query)->read(); $response = $client->query($query)->read();
var_dump($response); var_dump($response);
```
// Send "equal" query
Basic example for update/create/delete types of queries:
```php
use \RouterOS\Client;
use \RouterOS\Query;
// Initiate client with config object
$client = new Client([
'host' => '192.168.1.3',
'user' => 'admin',
'pass' => 'admin'
]);
// Send "equal" query with details about IP address which should be created
$query = $query =
(new Query('/ip/hotspot/ip-binding/add')) (new Query('/ip/hotspot/ip-binding/add'))
->equal('mac-address', '00:00:00:00:40:29') ->equal('mac-address', '00:00:00:00:40:29')
->equal('type', 'bypassed') ->equal('type', 'bypassed')
->equal('comment', 'testcomment'); ->equal('comment', 'testcomment');
// Send query and read response from RouterOS (ordinary answer to update/create/delete queries has empty body)
// Send query and read response from RouterOS (ordinary answer from update/create/delete queries has empty body)
$response = $client->query($query)->read(); $response = $client->query($query)->read();
var_dump($response);
```
If you need export all settings from router:
```php
use \RouterOS\Client;
// Initiate client with config object
$client = new Client([
'host' => '192.168.1.3',
'user' => 'admin',
'pass' => 'admin',
'ssh_port' => 22222,
]);
// Execute export command via ssh, because API /export method has a bug
$response = $client->export();
print_r($response);
``` ```
Examples with "where" conditions, "operations" and "tag": Examples with "where" conditions, "operations" and "tag":
@ -257,10 +308,10 @@ need to create a "Query" object whose first argument is the
required command, after this you can add the attributes of the required command, after this you can add the attributes of the
command to "Query" object. command to "Query" object.
More about attributes and "words" from which this attributes
More about attributes and "words" from which these attributes
should be created [here](https://wiki.mikrotik.com/wiki/Manual:API#Command_word). should be created [here](https://wiki.mikrotik.com/wiki/Manual:API#Command_word).
More about "expressions", "where" and other filters/modificators
More about "expressions", "where", "equal" and other filters/modifications
of your query you can find [here](https://wiki.mikrotik.com/wiki/Manual:API#Queries). of your query you can find [here](https://wiki.mikrotik.com/wiki/Manual:API#Queries).
Simple usage examples of Query class: Simple usage examples of Query class:
@ -271,6 +322,13 @@ use \RouterOS\Query;
// Get all installed packages (it may be enabled or disabled) // Get all installed packages (it may be enabled or disabled)
$query = new Query('/system/package/getall'); $query = new Query('/system/package/getall');
// Send "equal" query with details about IP address which should be created
$query =
(new Query('/ip/hotspot/ip-binding/add'))
->equal('mac-address', '00:00:00:00:40:29')
->equal('type', 'bypassed')
->equal('comment', 'testcomment');
// Set where interface is disabled and ID is ether1 (with tag 4) // Set where interface is disabled and ID is ether1 (with tag 4)
$query = $query =
(new Query('/interface/set')) (new Query('/interface/set'))
@ -285,7 +343,7 @@ $query =
->where('type', 'vlan') ->where('type', 'vlan')
->operations('|'); ->operations('|');
/// Get all routes that have non-empty comment
// Get all routes that have non-empty comment
$query = $query =
(new Query('/ip/route/print')) (new Query('/ip/route/print'))
->where('comment', '>', null); ->where('comment', '>', null);
@ -357,8 +415,8 @@ $query->operations('|');
// Enable interface (tag is 4) // Enable interface (tag is 4)
$query = new Query('/interface/set'); $query = new Query('/interface/set');
$query->where('disabled', 'no');
$query->where('.id', 'ether1');
$query->equal('disabled', 'no');
$query->equal('.id', 'ether1');
$query->tag(4); $query->tag(4);
// Or // Or
@ -396,8 +454,8 @@ $response = $client->query($query)->read();
## Read response as Iterator ## Read response as Iterator
By default original solution of this client is not optimized for
work with large amount of results, only for small count of lines
By default, original solution of this client is not optimized for
work with a large amount of results, only for small count of lines
in response from RouterOS API. in response from RouterOS API.
But some routers may have (for example) 30000+ records in But some routers may have (for example) 30000+ records in
@ -468,6 +526,75 @@ $client->query($query1)->query($query2)->query($query3);
$client->q($query1)->q($query2)->q($query3); $client->q($query1)->q($query2)->q($query3);
``` ```
## Known issues
### Unable to establish socket session, Operation timed out
This error means that the library cannot connect to your router,
it may mean router turned off (then need turn on), or the API service not enabled.
Go to `Mikrotik Router OS -> IP -> Services` and enable `api` service.
Or via command line:
```shell script
/ip service enable api
```
### How to update/remove/create something via API?
Instead of `->where()` method of `Query` class you need to
use `->equal()` method:
```php
// Create query which should remove security profile
$query = new \RouterOS\Query('/interface/wireless/security-profiles/remove');
// It will generate queries, which stared from "?" symbol:
$query->where('.id', '*1');
/*
// Sample with ->where() method
RouterOS\Query Object
(
[_attributes:RouterOS\Query:private] => Array
(
[0] => ?.id=*1
)
[_operations:RouterOS\Query:private] =>
[_tag:RouterOS\Query:private] =>
[_endpoint:RouterOS\Query:private] => /interface/wireless/security-profiles/remove
)
*/
// So, as you can see, instead of `->where()` need to use `->equal()`
// It will generate queries, which stared from "=" symbol:
$query->equal('.id', '*1');
/*
// Sample with ->equal() method
RouterOS\Query Object
(
[_attributes:RouterOS\Query:private] => Array
(
[0] => =.id=*1
)
[_operations:RouterOS\Query:private] =>
[_tag:RouterOS\Query:private] =>
[_endpoint:RouterOS\Query:private] => /interface/wireless/security-profiles/remove
)
*/
```
### Undefined character (any non-English languages)
RouterOS does not support national languages, only English (and API of RouterOS too).
You can try to reproduce it via web, for example add the comment to any
element of your system, then save and reload the page, you will see unreadable characters.
## Testing ## Testing
You can use my [other project](https://github.com/EvilFreelancer/docker-routeros) You can use my [other project](https://github.com/EvilFreelancer/docker-routeros)

15
composer.json

@ -33,22 +33,25 @@
"extra": { "extra": {
"laravel": { "laravel": {
"providers": [ "providers": [
"RouterOS\\Laravel\\ClientServiceProvider"
"RouterOS\\Laravel\\ServiceProvider"
], ],
"aliases": { "aliases": {
"RouterOS": "RouterOS\\Laravel\\ClientFacade"
"RouterOS": "RouterOS\\Laravel\\Facade"
} }
} }
}, },
"require": { "require": {
"php": "^7.2", "php": "^7.2",
"ext-sockets": "*"
"ext-sockets": "*",
"divineomega/php-ssh-connection": "^2.2"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^7.0",
"orchestra/testbench": "^3.0",
"limedeck/phpunit-detailed-printer": "^5.0",
"orchestra/testbench": "^4.0|^5.0",
"phpunit/phpunit": "^8.0",
"roave/security-advisories": "dev-master", "roave/security-advisories": "dev-master",
"squizlabs/php_codesniffer": "^3.5"
"squizlabs/php_codesniffer": "^3.5",
"larapack/dd": "^1.1"
}, },
"scripts": { "scripts": {
"test": "phpunit --coverage-clover clover.xml", "test": "phpunit --coverage-clover clover.xml",

44
configs/routeros-api.php

@ -1,13 +1,39 @@
<?php <?php
return [ return [
// 'host' => null,
// 'user' => null,
// 'pass' => null,
// 'port' => null,
'ssl' => false,
'legacy' => false,
'timeout' => 10,
'attempts' => 10,
'delay' => 1,
/*
|--------------------------------------------------------------------------
| Connection details
|--------------------------------------------------------------------------
|
| Here you may specify different information about your router, like
| hostname (or ip-address), username, password, port and ssl mode.
|
| SSH port should be set if you want to use "/export" command.
|
*/
'host' => '192.168.88.1', // Address of Mikrotik RouterOS
'user' => 'admin', // Username
'pass' => null, // Password
'port' => 8728, // RouterOS API port number for access (if not set use default or default with SSL if SSL enabled)
'ssl' => false, // Enable ssl support (if port is not set this parameter must change default port to ssl port)
'ssh_port' => 22, // Number of SSH port
/*
|--------------------------------------------------------------------------
| Optional connection settings of client
|--------------------------------------------------------------------------
|
| Settings bellow need to advanced tune of your connection, for example
| you may enable legacy mode by default, or change timeout of connection.
|
*/
'legacy' => false, // Support of legacy login scheme (true - pre 6.43, false - post 6.43)
'timeout' => 10, // Max timeout for answer from RouterOS
'attempts' => 10, // Count of attempts to establish TCP session
'delay' => 1, // Delay between attempts in seconds
]; ];

2
examples/bridge_hosts.php

@ -22,5 +22,5 @@ $client = new Client($config);
$query = new Query('/interface/bridge/host/print'); $query = new Query('/interface/bridge/host/print');
// Send query to RouterOS // Send query to RouterOS
$response = $client->write($query)->read();
$response = $client->query($query)->read();
print_r($response); print_r($response);

6
examples/different_queries.php

@ -14,12 +14,12 @@ $client = new Client([
]); ]);
for ($i = 0; $i < 10; $i++) { for ($i = 0; $i < 10; $i++) {
$response = $client->wr('/ip/address/print');
$response = $client->qr('/ip/address/print');
print_r($response); print_r($response);
$response = $client->wr('/ip/arp/print');
$response = $client->qr('/ip/arp/print');
print_r($response); print_r($response);
$response = $client->wr('/interface/print');
$response = $client->qr('/interface/print');
print_r($response); print_r($response);
} }

30
examples/export.php

@ -12,14 +12,34 @@ $config =
(new Config()) (new Config())
->set('host', '127.0.0.1') ->set('host', '127.0.0.1')
->set('pass', 'admin') ->set('pass', 'admin')
->set('user', 'admin');
->set('user', 'admin')
->set('ssh_port', 22222);
// Initiate client with config object // Initiate client with config object
$client = new Client($config); $client = new Client($config);
// Build query
// Execute export command via ssh
$response = $client->export();
dump($response);
/*
// In results you will see something like this
# jun/28/2020 16:31:21 by RouterOS 6.47
# software id =
#
#
#
/interface wireless security-profiles
set [ find default=yes ] supplicant-identity=MikroTik
/ip dhcp-client
add disabled=no interface=ether1
*/
// But here is another example
$query = new Query('/export'); $query = new Query('/export');
// Send query and read answer from RouterOS
$response = $client->write($query)->read(false);
print_r($response);
// Execute export command via ssh but in style of library
$response = $client->query($query)->read();
dump($response);

2
examples/interface_print.php

@ -21,7 +21,7 @@ $client = new Client($config);
$query = new Query('/interface/getall'); $query = new Query('/interface/getall');
// Send query to RouterOS // Send query to RouterOS
$request = $client->write($query);
$request = $client->query($query);
// Read answer from RouterOS // Read answer from RouterOS
$response = $client->read(); $response = $client->read();

2
examples/ip_address_print.php

@ -18,5 +18,5 @@ $client = new Client([
$query = new Query('/ip/address/print'); $query = new Query('/ip/address/print');
// Send query to RouterOS // Send query to RouterOS
$response = $client->write($query)->read();
$response = $client->query($query)->read();
print_r($response); print_r($response);

2
examples/ip_filrewall_address-list_print.php

@ -14,7 +14,7 @@ $client = new Client([
]); ]);
// Send query to RouterOS and parse response // Send query to RouterOS and parse response
$response = $client->write('/ip/firewall/address-list/print')->read();
$response = $client->query('/ip/firewall/address-list/print')->read();
// You could treat response as an array except using array_* function // You could treat response as an array except using array_* function

2
examples/queue_simple_print.php

@ -25,6 +25,6 @@ $ips = [
foreach ($ips as $ip) { foreach ($ips as $ip) {
$query = new Query('/queue/simple/print', ['?target=' . $ip . '/32']); $query = new Query('/queue/simple/print', ['?target=' . $ip . '/32']);
$response = $client->wr($query);
$response = $client->qr($query);
print_r($response); print_r($response);
} }

2
examples/queue_simple_print_v2.php

@ -24,7 +24,7 @@ $ips = [
]; ];
foreach ($ips as $ip) { foreach ($ips as $ip) {
$response = $client->wr([
$response = $client->qr([
'/queue/simple/print', '/queue/simple/print',
'?target=' . $ip . '/32' '?target=' . $ip . '/32'
]); ]);

4
examples/queue_simple_write.php

@ -12,8 +12,8 @@ $client = new Client([
'pass' => 'admin' 'pass' => 'admin'
]); ]);
$out = $client->write(['/queue/simple/add', '=name=test'])->read();
$out = $client->query(['/queue/simple/add', '=name=test'])->read();
print_r($out); print_r($out);
$out = $client->write(['/queue/simple/add', '=name=test'])->read();
$out = $client->query(['/queue/simple/add', '=name=test'])->read();
print_r($out); print_r($out);

14
examples/remove_security_profile.php

@ -0,0 +1,14 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
error_reporting(E_ALL);
// Create query which should remove security profile
$query = new \RouterOS\Query('/interface/wireless/security-profiles/remove');
// Here, instead of `->where()` need to use `->equal()`, it will generate queries,
// which stared from "=" symbol:
$query->where('.id', '*1');
$client = new \RouterOS\Client(['host' => '192.168.88.1', 'user' => 'admin', 'pass' => 'password']);
$response = $client->query($query)->read();

2
examples/system_package_print.php

@ -21,7 +21,7 @@ $client = new Client($config);
$query = new Query('/system/package/print'); $query = new Query('/system/package/print');
// Send query to RouterOS // Send query to RouterOS
$request = $client->write($query);
$request = $client->query($query);
// Read answer from RouterOS // Read answer from RouterOS
$response = $client->read(); $response = $client->read();

6
examples/vlans_bridge.php

@ -35,14 +35,14 @@ foreach ($vlans as $vlanId => $ports) {
// Add bridges // Add bridges
$query = new Query('/interface/bridge/add'); $query = new Query('/interface/bridge/add');
$query->add("=name=vlan$vlanId-bridge")->add('vlan-filtering=no'); $query->add("=name=vlan$vlanId-bridge")->add('vlan-filtering=no');
$response = $client->write($query)->read();
$response = $client->query($query)->read();
print_r($response); print_r($response);
// Add ports to bridge // Add ports to bridge
foreach ($ports as $port) { foreach ($ports as $port) {
$bridgePort = new Query('/interface/bridge/port/add'); $bridgePort = new Query('/interface/bridge/port/add');
$bridgePort->add("=bridge=vlan$vlanId-bridge")->add("=pvid=$vlanId")->add("=interface=ether$port"); $bridgePort->add("=bridge=vlan$vlanId-bridge")->add("=pvid=$vlanId")->add("=interface=ether$port");
$response = $client->write($bridgePort)->read();
$response = $client->query($bridgePort)->read();
print_r($response); print_r($response);
} }
@ -50,7 +50,7 @@ foreach ($vlans as $vlanId => $ports) {
foreach ($ports as $port) { foreach ($ports as $port) {
$vlan = new Query('/interface/bridge/vlan/add'); $vlan = new Query('/interface/bridge/vlan/add');
$vlan->add("=bridge=vlan$vlanId-bridge")->add("=untagged=ether$port")->add("=vlan-ids=$vlanId"); $vlan->add("=bridge=vlan$vlanId-bridge")->add("=untagged=ether$port")->add("=vlan-ids=$vlanId");
$response = $client->write($vlan)->read(false);
$response = $client->query($vlan)->read(false);
print_r($response); print_r($response);
} }

6
examples/vlans_bridge_v2.php

@ -34,7 +34,7 @@ foreach ($vlans as $vlanId => $ports) {
'vlan-filtering=no' 'vlan-filtering=no'
]); ]);
$response = $client->wr($query);
$response = $client->qr($query);
print_r($response); print_r($response);
// Add ports to bridge // Add ports to bridge
@ -45,7 +45,7 @@ foreach ($vlans as $vlanId => $ports) {
"=interface=ether$port" "=interface=ether$port"
]); ]);
$response = $client->wr($bridgePort);
$response = $client->qr($bridgePort);
print_r($response); print_r($response);
} }
@ -57,7 +57,7 @@ foreach ($vlans as $vlanId => $ports) {
"=vlan-ids=$vlanId" "=vlan-ids=$vlanId"
]); ]);
$response = $client->wr($vlan);
$response = $client->qr($vlan);
print_r($response); print_r($response);
} }

6
examples/vlans_bridge_v3.php

@ -28,7 +28,7 @@ $vlans = [
foreach ($vlans as $vlanId => $ports) { foreach ($vlans as $vlanId => $ports) {
// Add bridges // Add bridges
$response = $client->wr([
$response = $client->qr([
'/interface/bridge/add', '/interface/bridge/add',
"=name=vlan$vlanId-bridge", "=name=vlan$vlanId-bridge",
'vlan-filtering=no' 'vlan-filtering=no'
@ -37,7 +37,7 @@ foreach ($vlans as $vlanId => $ports) {
// Add ports to bridge // Add ports to bridge
foreach ($ports as $port) { foreach ($ports as $port) {
$response = $client->wr([
$response = $client->qr([
'/interface/bridge/port/add', '/interface/bridge/port/add',
"=bridge=vlan$vlanId-bridge", "=bridge=vlan$vlanId-bridge",
"=pvid=$vlanId", "=pvid=$vlanId",
@ -48,7 +48,7 @@ foreach ($vlans as $vlanId => $ports) {
// Add untagged ports to bridge with tagging // Add untagged ports to bridge with tagging
foreach ($ports as $port) { foreach ($ports as $port) {
$response = $client->wr([
$response = $client->qr([
'/interface/bridge/vlan/add', '/interface/bridge/vlan/add',
"=bridge=vlan$vlanId-bridge", "=bridge=vlan$vlanId-bridge",
"=untagged=ether$port", "=untagged=ether$port",

14
phpunit.xml

@ -1,10 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./vendor/autoload.php" colors="true">
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
printerClass="LimeDeck\Testing\Printer"
processIsolation="false"
stopOnFailure="true">
<filter> <filter>
<whitelist processUncoveredFilesFromWhitelist="true"> <whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory> <directory suffix=".php">./src</directory>
<exclude> <exclude>
<directory suffix=".php">./tests</directory>
<directory suffix="Test.php">./tests</directory>
</exclude> </exclude>
</whitelist> </whitelist>
</filter> </filter>
@ -22,5 +31,6 @@
<env name="ROS_PASS" value="admin"/> <env name="ROS_PASS" value="admin"/>
<env name="ROS_PORT_MODERN" value="8728"/> <env name="ROS_PORT_MODERN" value="8728"/>
<env name="ROS_PORT_LEGACY" value="18728"/> <env name="ROS_PORT_LEGACY" value="18728"/>
<env name="ROS_SSH_PORT" value="22222"/>
</php> </php>
</phpunit> </phpunit>

2
preconf.tcl

@ -7,7 +7,7 @@ set port [lindex $argv 0]
spawn telnet localhost $port spawn telnet localhost $port
expect "Login: " expect "Login: "
send "admin+c\n"
send "admin+etc\n"
expect "Password: " expect "Password: "
send "\n" send "\n"
expect "]:" expect "]:"

15
src/APIConnector.php

@ -20,17 +20,26 @@ class APIConnector
protected $stream; protected $stream;
/** /**
* Constructor
* APIConnector constructor.
* *
* @param StreamInterface $stream
* @param \RouterOS\Interfaces\StreamInterface $stream
*/ */
public function __construct(StreamInterface $stream) public function __construct(StreamInterface $stream)
{ {
$this->stream = $stream; $this->stream = $stream;
} }
/** /**
* Close stream connection
*
* @return void
*/
public function close(): void
{
$this->stream->close();
}
/**
* Reads a WORD from the stream * Reads a WORD from the stream
* *
* WORDs are part of SENTENCE. Each WORD has to be encoded in certain way - length of the WORD followed by WORD content. * WORDs are part of SENTENCE. Each WORD has to be encoded in certain way - length of the WORD followed by WORD content.

6
src/APILengthCoDec.php

@ -3,8 +3,10 @@
namespace RouterOS; namespace RouterOS;
use DomainException; use DomainException;
use OverflowException;
use RouterOS\Interfaces\StreamInterface; use RouterOS\Interfaces\StreamInterface;
use RouterOS\Helpers\BinaryStringHelper; use RouterOS\Helpers\BinaryStringHelper;
use UnexpectedValueException;
/** /**
* class APILengthCoDec * class APILengthCoDec
@ -169,7 +171,7 @@ class APILengthCoDec
// How can we test it ? // How can we test it ?
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
throw new \OverflowException("Your system is using 32 bits integers, cannot decode this value ($firstByte) on this system");
throw new OverflowException("Your system is using 32 bits integers, cannot decode this value ($firstByte) on this system");
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
@ -189,6 +191,6 @@ class APILengthCoDec
// Now the only solution is 5 most significance bits are set to 1 (11111xxx) // Now the only solution is 5 most significance bits are set to 1 (11111xxx)
// This is a control word, not implemented by Mikrotik for the moment // This is a control word, not implemented by Mikrotik for the moment
throw new \UnexpectedValueException('Control Word found');
throw new UnexpectedValueException('Control Word found');
} }
} }

168
src/Client.php

@ -2,18 +2,17 @@
namespace RouterOS; namespace RouterOS;
use DivineOmega\SSHConnection\SSHConnection;
use RouterOS\Exceptions\ClientException; use RouterOS\Exceptions\ClientException;
use RouterOS\Exceptions\ConfigException; use RouterOS\Exceptions\ConfigException;
use RouterOS\Exceptions\QueryException;
use RouterOS\Helpers\ArrayHelper;
use RouterOS\Interfaces\ClientInterface;
use RouterOS\Interfaces\QueryInterface; use RouterOS\Interfaces\QueryInterface;
use RouterOS\Helpers\ArrayHelper;
use function array_keys; use function array_keys;
use function array_shift; use function array_shift;
use function chr; use function chr;
use function count; use function count;
use function is_array; use function is_array;
use function is_string;
use function md5; use function md5;
use function pack; use function pack;
use function preg_match_all; use function preg_match_all;
@ -35,26 +34,33 @@ class Client implements Interfaces\ClientInterface
* *
* @var \RouterOS\Config * @var \RouterOS\Config
*/ */
private $_config;
private $config;
/** /**
* API communication object * API communication object
* *
* @var \RouterOS\APIConnector * @var \RouterOS\APIConnector
*/ */
private $connector;
private $_connector;
/**
* Some strings with custom output
*
* @var string
*/
private $customOutput;
/** /**
* Client constructor. * Client constructor.
* *
* @param array|\RouterOS\Interfaces\ConfigInterface $config
* @param array|\RouterOS\Interfaces\ConfigInterface $config Array with configuration or Config object
* @param bool $autoConnect If false it will skip auto-connect stage if not need to instantiate connection
* *
* @throws \RouterOS\Exceptions\ClientException * @throws \RouterOS\Exceptions\ClientException
* @throws \RouterOS\Exceptions\ConfigException * @throws \RouterOS\Exceptions\ConfigException
* @throws \RouterOS\Exceptions\QueryException * @throws \RouterOS\Exceptions\QueryException
*/ */
public function __construct($config)
public function __construct($config, bool $autoConnect = true)
{ {
// If array then need create object // If array then need create object
if (is_array($config)) { if (is_array($config)) {
@ -67,7 +73,12 @@ class Client implements Interfaces\ClientInterface
} }
// Save config if everything is okay // Save config if everything is okay
$this->_config = $config;
$this->config = $config;
// Skip next step if not need to instantiate connection
if (false === $autoConnect) {
return;
}
// Throw error if cannot to connect // Throw error if cannot to connect
if (false === $this->connect()) { if (false === $this->connect()) {
@ -85,49 +96,24 @@ class Client implements Interfaces\ClientInterface
*/ */
private function config(string $parameter) private function config(string $parameter)
{ {
return $this->_config->get($parameter);
}
/**
* Send write query to RouterOS
*
* @param string|array|\RouterOS\Query $query
*
* @return \RouterOS\Client
* @throws \RouterOS\Exceptions\QueryException
* @deprecated
*/
public function write($query): Client
{
if (is_string($query)) {
$query = new Query($query);
} elseif (is_array($query)) {
$endpoint = array_shift($query);
$query = new Query($endpoint, $query);
}
if (!$query instanceof Query) {
throw new QueryException('Parameters cannot be processed');
}
// Submit query to RouterOS
return $this->writeRAW($query);
return $this->config->get($parameter);
} }
/** /**
* Send write query to RouterOS (modern version of write) * Send write query to RouterOS (modern version of write)
* *
* @param string|\RouterOS\Query $endpoint Path of API query or Query object
* @param array|string|\RouterOS\Interfaces\QueryInterface $endpoint Path of API query or Query object
* @param array|null $where List of where filters * @param array|null $where List of where filters
* @param string|null $operations Some operations which need make on response * @param string|null $operations Some operations which need make on response
* @param string|null $tag Mark query with tag * @param string|null $tag Mark query with tag
* *
* @return \RouterOS\Client
* @return \RouterOS\Interfaces\ClientInterface
* @throws \RouterOS\Exceptions\QueryException * @throws \RouterOS\Exceptions\QueryException
* @throws \RouterOS\Exceptions\ClientException * @throws \RouterOS\Exceptions\ClientException
* @throws \RouterOS\Exceptions\ConfigException
* @since 1.0.0 * @since 1.0.0
*/ */
public function query($endpoint, array $where = null, string $operations = null, string $tag = null): Client
public function query($endpoint, array $where = null, string $operations = null, string $tag = null): ClientInterface
{ {
// If endpoint is string then build Query object // If endpoint is string then build Query object
$query = ($endpoint instanceof Query) $query = ($endpoint instanceof Query)
@ -169,10 +155,10 @@ class Client implements Interfaces\ClientInterface
* @param \RouterOS\Interfaces\QueryInterface $query * @param \RouterOS\Interfaces\QueryInterface $query
* *
* @return \RouterOS\Query * @return \RouterOS\Query
* @throws \RouterOS\Exceptions\ClientException
* @throws \RouterOS\Exceptions\QueryException * @throws \RouterOS\Exceptions\QueryException
* @throws \RouterOS\Exceptions\ClientException
*/ */
private function preQuery(array $item, Query $query): Query
private function preQuery(array $item, QueryInterface $query): QueryInterface
{ {
// Null by default // Null by default
$key = null; $key = null;
@ -199,41 +185,66 @@ class Client implements Interfaces\ClientInterface
/** /**
* Send write query object to RouterOS * Send write query object to RouterOS
* *
* @param \RouterOS\Query $query
* @param \RouterOS\Interfaces\QueryInterface $query
* *
* @return \RouterOS\Client
* @return \RouterOS\Interfaces\ClientInterface
* @throws \RouterOS\Exceptions\QueryException * @throws \RouterOS\Exceptions\QueryException
* @throws \RouterOS\Exceptions\ConfigException
* @since 1.0.0 * @since 1.0.0
*/ */
private function writeRAW(Query $query): Client
private function writeRAW(QueryInterface $query): ClientInterface
{ {
$commands = $query->getQuery();
// Check if first command is export
if (strpos($commands[0], '/export') === 0) {
// Convert export command with all arguments to valid SSH command
$arguments = explode('/', $commands[0]);
unset($arguments[1]);
$arguments = implode(' ', $arguments);
// Call the router via ssh and store output of export
$this->customOutput = $this->export($arguments);
// Return current object
return $this;
}
// Send commands via loop to router // Send commands via loop to router
foreach ($query->getQuery() as $command) {
$this->_connector->writeWord(trim($command));
foreach ($commands as $command) {
$this->connector->writeWord(trim($command));
} }
// Write zero-terminator (empty string) // Write zero-terminator (empty string)
$this->_connector->writeWord('');
$this->connector->writeWord('');
// Return current object
return $this; return $this;
} }
/** /**
* Read RAW response from RouterOS
* Read RAW response from RouterOS, it can be /export command results also, not only array from API
* *
* @return array
* @return array|string
* @since 1.0.0 * @since 1.0.0
*/ */
private function readRAW(): array
private function readRAW()
{ {
// By default response is empty // By default response is empty
$response = []; $response = [];
// We have to wait a !done or !fatal // We have to wait a !done or !fatal
$lastReply = false; $lastReply = false;
// Convert strings to array and return results
if ($this->isCustomOutput()) {
// Return RAW configuration
return $this->customOutput;
}
// Read answer from socket in loop // Read answer from socket in loop
while (true) { while (true) {
$word = $this->_connector->readWord();
$word = $this->connector->readWord();
if ('' === $word) { if ('' === $word) {
if ($lastReply) { if ($lastReply) {
@ -270,7 +281,7 @@ class Client implements Interfaces\ClientInterface
* Reply ends with a complete !done or !fatal block (ended with 'empty line') * Reply ends with a complete !done or !fatal block (ended with 'empty line')
* A !fatal block precedes TCP connexion close * A !fatal block precedes TCP connexion close
* *
* @param bool $parse
* @param bool $parse If need parse output to array
* *
* @return mixed * @return mixed
*/ */
@ -279,6 +290,12 @@ class Client implements Interfaces\ClientInterface
// Read RAW response // Read RAW response
$response = $this->readRAW(); $response = $this->readRAW();
// Return RAW configuration if custom output is set
if ($this->isCustomOutput()) {
$this->customOutput = null;
return $response;
}
// Parse results and return // Parse results and return
return $parse ? $this->rosario($response) : $response; return $parse ? $this->rosario($response) : $response;
} }
@ -447,7 +464,7 @@ class Client implements Interfaces\ClientInterface
// => problem with legacy version, swap it and retry // => problem with legacy version, swap it and retry
// Only tested with ROS pre 6.43, will test with post 6.43 => this could make legacy parameter obsolete? // Only tested with ROS pre 6.43, will test with post 6.43 => this could make legacy parameter obsolete?
if ($legacyRetry && $this->isLegacy($response)) { if ($legacyRetry && $this->isLegacy($response)) {
$this->_config->set('legacy', true);
$this->config->set('legacy', true);
return $this->login(); return $this->login();
} }
@ -468,7 +485,7 @@ class Client implements Interfaces\ClientInterface
* @return bool * @return bool
* @throws \RouterOS\Exceptions\ConfigException * @throws \RouterOS\Exceptions\ConfigException
*/ */
private function isLegacy(array &$response): bool
private function isLegacy(array $response): bool
{ {
return count($response) > 1 && $response[0] === '!done' && !$this->config('legacy'); return count($response) > 1 && $response[0] === '!done' && !$this->config('legacy');
} }
@ -481,7 +498,7 @@ class Client implements Interfaces\ClientInterface
* @throws \RouterOS\Exceptions\ConfigException * @throws \RouterOS\Exceptions\ConfigException
* @throws \RouterOS\Exceptions\QueryException * @throws \RouterOS\Exceptions\QueryException
*/ */
private function connect(): bool
public function connect(): bool
{ {
// By default we not connected // By default we not connected
$connected = false; $connected = false;
@ -494,7 +511,7 @@ class Client implements Interfaces\ClientInterface
// If socket is active // If socket is active
if (null !== $this->getSocket()) { if (null !== $this->getSocket()) {
$this->_connector = new APIConnector(new Streams\ResourceStream($this->getSocket()));
$this->connector = new APIConnector(new Streams\ResourceStream($this->getSocket()));
// If we logged in then exit from loop // If we logged in then exit from loop
if (true === $this->login()) { if (true === $this->login()) {
$connected = true; $connected = true;
@ -512,4 +529,43 @@ class Client implements Interfaces\ClientInterface
// Return status of connection // Return status of connection
return $connected; return $connected;
} }
/**
* Check if custom output is not empty
*
* @return bool
*/
private function isCustomOutput(): bool
{
return $this->customOutput !== null;
}
/**
* Execute export command on remote host, it also will be used
* if "/export" command passed to query.
*
* @param string|null $arguments String with arguments which should be passed to export command
*
* @return string
* @throws \RouterOS\Exceptions\ConfigException
* @since 1.3.0
*/
public function export(string $arguments = null): string
{
// Connect to remote host
$connection =
(new SSHConnection())
->timeout($this->config('timeout'))
->to($this->config('host'))
->onPort($this->config('ssh_port'))
->as($this->config('user') . '+etc')
->withPassword($this->config('pass'))
->connect();
// Run export command
$command = $connection->run('/export' . ' ' . $arguments);
// Return the output
return $command->getOutput();
}
} }

106
src/Config.php

@ -6,6 +6,7 @@ use RouterOS\Exceptions\ConfigException;
use RouterOS\Helpers\ArrayHelper; use RouterOS\Helpers\ArrayHelper;
use RouterOS\Helpers\TypeHelper; use RouterOS\Helpers\TypeHelper;
use RouterOS\Interfaces\ConfigInterface; use RouterOS\Interfaces\ConfigInterface;
use function gettype;
/** /**
* Class Config with array of parameters * Class Config with array of parameters
@ -16,16 +17,73 @@ use RouterOS\Interfaces\ConfigInterface;
class Config implements ConfigInterface class Config implements ConfigInterface
{ {
/** /**
* By default legacy login on RouterOS pre-6.43 is not supported
*/
public const LEGACY = false;
/**
* Default port number
*/
public const PORT = 8728;
/**
* Default ssl port number
*/
public const PORT_SSL = 8729;
/**
* Do not use SSL by default
*/
public const SSL = false;
/**
* Max timeout for answer from router
*/
public const TIMEOUT = 10;
/**
* Count of reconnect attempts
*/
public const ATTEMPTS = 10;
/**
* Delay between attempts in seconds
*/
public const ATTEMPTS_DELAY = 1;
/**
* Delay between attempts in seconds
*/
public const SSH_PORT = 22;
/**
* List of allowed parameters of config
*/
public const ALLOWED = [
'host' => 'string', // Address of Mikrotik RouterOS
'user' => 'string', // Username
'pass' => 'string', // Password
'port' => 'integer', // RouterOS API port number for access (if not set use default or default with SSL if SSL enabled)
'ssl' => 'boolean', // Enable ssl support (if port is not set this parameter must change default port to ssl port)
'legacy' => 'boolean', // Support of legacy login scheme (true - pre 6.43, false - post 6.43)
'timeout' => 'integer', // Max timeout for answer from RouterOS
'attempts' => 'integer', // Count of attempts to establish TCP session
'delay' => 'integer', // Delay between attempts in seconds
'ssh_port' => 'integer', // Number of SSH port
];
/**
* Array of parameters (with some default values) * Array of parameters (with some default values)
* *
* @var array * @var array
*/ */
private $_parameters = [ private $_parameters = [
'legacy' => Client::LEGACY,
'ssl' => Client::SSL,
'timeout' => Client::TIMEOUT,
'attempts' => Client::ATTEMPTS,
'delay' => Client::ATTEMPTS_DELAY
'legacy' => self::LEGACY,
'ssl' => self::SSL,
'timeout' => self::TIMEOUT,
'attempts' => self::ATTEMPTS,
'delay' => self::ATTEMPTS_DELAY,
'ssh_port' => self::SSH_PORT,
]; ];
/** /**
@ -44,15 +102,11 @@ class Config implements ConfigInterface
} }
/** /**
* Set parameter into array
*
* @param string $name
* @param mixed $value
* @inheritDoc
* *
* @return \RouterOS\Config
* @throws \RouterOS\Exceptions\ConfigException
* @throws \RouterOS\Exceptions\ConfigException when name of configuration key is invalid or not allowed
*/ */
public function set(string $name, $value): Config
public function set(string $name, $value): ConfigInterface
{ {
// Check of key in array // Check of key in array
if (ArrayHelper::checkIfKeyNotExist($name, self::ALLOWED)) { if (ArrayHelper::checkIfKeyNotExist($name, self::ALLOWED)) {
@ -60,8 +114,8 @@ class Config implements ConfigInterface
} }
// Check what type has this value // Check what type has this value
if (TypeHelper::checkIfTypeMismatch(\gettype($value), self::ALLOWED[$name])) {
throw new ConfigException("Parameter '$name' has wrong type '" . \gettype($value) . "' but should be '" . self::ALLOWED[$name] . "'");
if (TypeHelper::checkIfTypeMismatch(gettype($value), self::ALLOWED[$name])) {
throw new ConfigException("Parameter '$name' has wrong type '" . gettype($value) . "' but should be '" . self::ALLOWED[$name] . "'");
} }
// Save value to array // Save value to array
@ -83,21 +137,18 @@ class Config implements ConfigInterface
if ($parameter === 'port' && (!isset($this->_parameters['port']) || null === $this->_parameters['port'])) { if ($parameter === 'port' && (!isset($this->_parameters['port']) || null === $this->_parameters['port'])) {
// then use default with or without ssl encryption // then use default with or without ssl encryption
return (isset($this->_parameters['ssl']) && $this->_parameters['ssl']) return (isset($this->_parameters['ssl']) && $this->_parameters['ssl'])
? Client::PORT_SSL
: Client::PORT;
? self::PORT_SSL
: self::PORT;
} }
return null; return null;
} }
/** /**
* Remove parameter from array by name
*
* @param string $name
* @inheritDoc
* *
* @return \RouterOS\Config
* @throws \RouterOS\Exceptions\ConfigException
* @throws \RouterOS\Exceptions\ConfigException when parameter is not allowed
*/ */
public function delete(string $name): Config
public function delete(string $name): ConfigInterface
{ {
// Check of key in array // Check of key in array
if (ArrayHelper::checkIfKeyNotExist($name, self::ALLOWED)) { if (ArrayHelper::checkIfKeyNotExist($name, self::ALLOWED)) {
@ -111,12 +162,9 @@ class Config implements ConfigInterface
} }
/** /**
* Return parameter of current config by name
*
* @param string $name
* @inheritDoc
* *
* @return mixed
* @throws \RouterOS\Exceptions\ConfigException
* @throws \RouterOS\Exceptions\ConfigException when parameter is not allowed
*/ */
public function get(string $name) public function get(string $name)
{ {
@ -129,9 +177,7 @@ class Config implements ConfigInterface
} }
/** /**
* Return array with all parameters of configuration
*
* @return array
* @inheritDoc
*/ */
public function getParameters(): array public function getParameters(): array
{ {

2
src/Helpers/ArrayHelper.php

@ -15,6 +15,7 @@ class ArrayHelper
* *
* @param string $key * @param string $key
* @param array $array * @param array $array
*
* @return bool * @return bool
*/ */
public static function checkIfKeyNotExist(string $key, array $array): bool public static function checkIfKeyNotExist(string $key, array $array): bool
@ -27,6 +28,7 @@ class ArrayHelper
* *
* @param array $keys * @param array $keys
* @param array $array * @param array $array
*
* @return array|bool Return true if all fine, and string with name of key which was not found * @return array|bool Return true if all fine, and string with name of key which was not found
*/ */
public static function checkIfKeysNotExist(array $keys, array $array) public static function checkIfKeysNotExist(array $keys, array $array)

77
src/Interfaces/ClientInterface.php

@ -2,9 +2,6 @@
namespace RouterOS\Interfaces; namespace RouterOS\Interfaces;
use RouterOS\Client;
use RouterOS\Query;
/** /**
* Interface ClientInterface * Interface ClientInterface
* *
@ -14,41 +11,6 @@ use RouterOS\Query;
interface ClientInterface interface ClientInterface
{ {
/** /**
* By default legacy login on RouterOS pre-6.43 is not supported
*/
public const LEGACY = false;
/**
* Default port number
*/
public const PORT = 8728;
/**
* Default ssl port number
*/
public const PORT_SSL = 8729;
/**
* Do not use SSL by default
*/
public const SSL = false;
/**
* Max timeout for answer from router
*/
public const TIMEOUT = 10;
/**
* Count of reconnect attempts
*/
public const ATTEMPTS = 10;
/**
* Delay between attempts in seconds
*/
public const ATTEMPTS_DELAY = 1;
/**
* Return socket resource if is exist * Return socket resource if is exist
* *
* @return resource * @return resource
@ -58,29 +20,42 @@ interface ClientInterface
/** /**
* Read answer from server after query was executed * Read answer from server after query was executed
* *
* @param bool $parse
* @return mixed
*/
public function read(bool $parse);
/**
* Send write query to RouterOS
* A Mikrotik reply is formed of blocks
* Each block starts with a word, one of ('!re', '!trap', '!done', '!fatal')
* Each block end with an zero byte (empty line)
* Reply ends with a complete !done or !fatal block (ended with 'empty line')
* A !fatal block precedes TCP connexion close
* *
* @param string|array|\RouterOS\Query $query
* @return \RouterOS\Client
* @param bool $parse If need parse output to array
*
* @return mixed
*/ */
public function write($query): Client;
public function read(bool $parse = true);
/** /**
* Send write query to RouterOS (modern version of write) * Send write query to RouterOS (modern version of write)
* *
* @param string|Query $endpoint Path of API query or Query object
* @param array|string|\RouterOS\Interfaces\QueryInterface $endpoint Path of API query or Query object
* @param array|null $where List of where filters * @param array|null $where List of where filters
* @param string|null $operations Some operations which need make on response * @param string|null $operations Some operations which need make on response
* @param string|null $tag Mark query with tag * @param string|null $tag Mark query with tag
* @return \RouterOS\Client
*
* @return \RouterOS\Interfaces\ClientInterface
* @throws \RouterOS\Exceptions\QueryException * @throws \RouterOS\Exceptions\QueryException
* @throws \RouterOS\Exceptions\ClientException
* @throws \RouterOS\Exceptions\ConfigException
* @since 1.0.0 * @since 1.0.0
*/ */
public function query($endpoint, array $where, string $operations, string $tag): Client;
public function query($endpoint, array $where, string $operations, string $tag): ClientInterface;
/**
* Execute export command on remote host
*
* @return string
* @throws \RouterOS\Exceptions\ConfigException
* @throws \RuntimeException
*
* @since 1.3.0
*/
public function export(): string;
} }

34
src/Interfaces/ConfigInterface.php

@ -2,8 +2,6 @@
namespace RouterOS\Interfaces; namespace RouterOS\Interfaces;
use RouterOS\Config;
/** /**
* Interface ConfigInterface * Interface ConfigInterface
* *
@ -13,47 +11,23 @@ use RouterOS\Config;
interface ConfigInterface interface ConfigInterface
{ {
/** /**
* List of allowed parameters of config
*/
public const ALLOWED = [
// Address of Mikrotik RouterOS
'host' => 'string',
// Username
'user' => 'string',
// Password
'pass' => 'string',
// RouterOS API port number for access (if not set use default or default with SSL if SSL enabled)
'port' => 'integer',
// Enable ssl support (if port is not set this parameter must change default port to ssl port)
'ssl' => 'boolean',
// Support of legacy login scheme (true - pre 6.43, false - post 6.43)
'legacy' => 'boolean',
// Max timeout for answer from RouterOS
'timeout' => 'integer',
// Count of attempts to establish TCP session
'attempts' => 'integer',
// Delay between attempts in seconds
'delay' => 'integer',
];
/**
* Set parameter into array * Set parameter into array
* *
* @param string $name * @param string $name
* @param mixed $value * @param mixed $value
* *
* @return \RouterOS\Config
* @return \RouterOS\Interfaces\ConfigInterface
*/ */
public function set(string $name, $value): Config;
public function set(string $name, $value): ConfigInterface;
/** /**
* Remove parameter from array by name * Remove parameter from array by name
* *
* @param string $parameter * @param string $parameter
* *
* @return \RouterOS\Config
* @return \RouterOS\Interfaces\ConfigInterface
*/ */
public function delete(string $parameter): Config;
public function delete(string $parameter): ConfigInterface;
/** /**
* Return parameter of current config by name * Return parameter of current config by name

2
src/Interfaces/QueryInterface.php

@ -18,7 +18,7 @@ interface QueryInterface
* @param bool|string|int $operator It may be one from list [-,=,>,<] * @param bool|string|int $operator It may be one from list [-,=,>,<]
* *
* @return \RouterOS\Interfaces\QueryInterface * @return \RouterOS\Interfaces\QueryInterface
* @throws \RouterOS\Exceptions\ClientException
* @throws \RouterOS\Exceptions\QueryException
* @since 1.0.0 * @since 1.0.0
*/ */
public function where(string $key, $operator = '=', $value = null): QueryInterface; public function where(string $key, $operator = '=', $value = null): QueryInterface;

2
src/Interfaces/StreamInterface.php

@ -44,5 +44,5 @@ interface StreamInterface
* *
* @return void * @return void
*/ */
public function close();
public function close(): void;
} }

24
src/Laravel/ClientWrapper.php

@ -1,24 +0,0 @@
<?php
namespace RouterOS\Laravel;
use RouterOS\Client;
class ClientWrapper
{
/**
* @param array $params
*
* @return \RouterOS\Client
* @throws \RouterOS\Exceptions\ClientException
* @throws \RouterOS\Exceptions\ConfigException
* @throws \RouterOS\Exceptions\QueryException
*/
public function getClient(array $params = []): Client
{
$configs = config('routeros-api');
$configs = array_merge($configs, $params);
return new Client($configs);
}
}

6
src/Laravel/ClientFacade.php → src/Laravel/Facade.php

@ -2,9 +2,9 @@
namespace RouterOS\Laravel; namespace RouterOS\Laravel;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Facades\Facade as BaseFacade;
class ClientFacade extends Facade
class Facade extends BaseFacade
{ {
/** /**
* Get the registered name of the component. * Get the registered name of the component.
@ -13,6 +13,6 @@ class ClientFacade extends Facade
*/ */
protected static function getFacadeAccessor(): string protected static function getFacadeAccessor(): string
{ {
return ClientWrapper::class;
return Wrapper::class;
} }
} }

4
src/Laravel/ClientServiceProvider.php → src/Laravel/ServiceProvider.php

@ -4,7 +4,7 @@ namespace RouterOS\Laravel;
use Illuminate\Support\ServiceProvider as BaseServiceProvider; use Illuminate\Support\ServiceProvider as BaseServiceProvider;
class ClientServiceProvider extends BaseServiceProvider
class ServiceProvider extends BaseServiceProvider
{ {
/** /**
* Bootstrap any application services. * Bootstrap any application services.
@ -29,6 +29,6 @@ class ClientServiceProvider extends BaseServiceProvider
__DIR__ . '/../../configs/routeros-api.php', 'routeros-api' __DIR__ . '/../../configs/routeros-api.php', 'routeros-api'
); );
$this->app->bind(ClientWrapper::class);
$this->app->bind(Wrapper::class);
} }
} }

63
src/Laravel/Wrapper.php

@ -0,0 +1,63 @@
<?php
namespace RouterOS\Laravel;
use RouterOS\Client;
use RouterOS\Config;
use RouterOS\Interfaces\ClientInterface;
use RouterOS\Interfaces\ConfigInterface;
class Wrapper
{
/**
* Alias for \RouterOS::client() method
*
* @param array $params
*
* @return \RouterOS\Client
* @throws \RouterOS\Exceptions\ClientException
* @throws \RouterOS\Exceptions\ConfigException
* @throws \RouterOS\Exceptions\QueryException
* @deprecated
* @codeCoverageIgnore
*/
public function getClient(array $params = []): ClientInterface
{
return $this->client($params);
}
/**
* Get configs of library
*
* @param array $params
*
* @return \RouterOS\Interfaces\ConfigInterface
* @throws \RouterOS\Exceptions\ConfigException
*/
public function config(array $params = []): ConfigInterface
{
$config = config('routeros-api');
$config = array_merge($config, $params);
$config = new Config($config);
return $config;
}
/**
* Instantiate client object
*
* @param array $params
* @param bool $autoConnect
*
* @return \RouterOS\Interfaces\ClientInterface
* @throws \RouterOS\Exceptions\ClientException
* @throws \RouterOS\Exceptions\ConfigException
* @throws \RouterOS\Exceptions\QueryException
*/
public function client(array $params = [], bool $autoConnect = true): ClientInterface
{
$config = $this->config($params);
return new Client($config, $autoConnect);
}
}

10
src/Query.php

@ -2,9 +2,11 @@
namespace RouterOS; namespace RouterOS;
use RouterOS\Exceptions\ClientException;
use RouterOS\Exceptions\QueryException; use RouterOS\Exceptions\QueryException;
use RouterOS\Interfaces\QueryInterface; use RouterOS\Interfaces\QueryInterface;
use function in_array;
use function is_array;
use function is_string;
/** /**
* Class Query for building queries * Class Query for building queries
@ -62,10 +64,10 @@ class Query implements QueryInterface
*/ */
public function __construct($endpoint, array $attributes = []) public function __construct($endpoint, array $attributes = [])
{ {
if (\is_string($endpoint)) {
if (is_string($endpoint)) {
$this->setEndpoint($endpoint); $this->setEndpoint($endpoint);
$this->setAttributes($attributes); $this->setAttributes($attributes);
} elseif (\is_array($endpoint)) {
} elseif (is_array($endpoint)) {
$query = array_shift($endpoint); $query = array_shift($endpoint);
$this->setEndpoint($query); $this->setEndpoint($query);
$this->setAttributes($endpoint); $this->setAttributes($endpoint);
@ -128,7 +130,7 @@ class Query implements QueryInterface
if (null !== $operator && null !== $value) { if (null !== $operator && null !== $value) {
// If operator is available in list // If operator is available in list
if (\in_array($operator, self::AVAILABLE_OPERATORS, true)) {
if (in_array($operator, self::AVAILABLE_OPERATORS, true)) {
$key = $operator . $key; $key = $operator . $key;
} else { } else {
throw new QueryException('Operator "' . $operator . '" in not in allowed list [' . implode(',', self::AVAILABLE_OPERATORS) . ']'); throw new QueryException('Operator "' . $operator . '" in not in allowed list [' . implode(',', self::AVAILABLE_OPERATORS) . ']');

27
src/ResponseIterator.php

@ -2,10 +2,15 @@
namespace RouterOS; namespace RouterOS;
use \Iterator,
\ArrayAccess,
\Countable,
\Serializable;
use Iterator;
use ArrayAccess;
use Countable;
use Serializable;
use function array_keys;
use function array_slice;
use function count;
use function serialize;
use function unserialize;
/** /**
* This class was created by memory save reasons, it convert response * This class was created by memory save reasons, it convert response
@ -35,7 +40,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable
* *
* @var array * @var array
*/ */
private $raw = [];
private $raw;
/** /**
* Initial value of array position * Initial value of array position
@ -95,7 +100,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable
/** /**
* Move forward to next element * Move forward to next element
*/ */
public function next()
public function next(): void
{ {
++$this->current; ++$this->current;
} }
@ -103,7 +108,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable
/** /**
* Previous value * Previous value
*/ */
public function prev()
public function prev(): void
{ {
--$this->current; --$this->current;
} }
@ -165,7 +170,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable
/** /**
* Rewind the Iterator to the first element * Rewind the Iterator to the first element
*/ */
public function rewind()
public function rewind(): void
{ {
$this->current = 0; $this->current = 0;
} }
@ -176,7 +181,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable
* @param mixed $offset * @param mixed $offset
* @param mixed $value * @param mixed $value
*/ */
public function offsetSet($offset, $value)
public function offsetSet($offset, $value): void
{ {
if (null === $offset) { if (null === $offset) {
$this->parsed[] = $value; $this->parsed[] = $value;
@ -202,7 +207,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable
* *
* @param mixed $offset * @param mixed $offset
*/ */
public function offsetUnset($offset)
public function offsetUnset($offset): void
{ {
unset($this->parsed[$offset], $this->raw[$offset]); unset($this->parsed[$offset], $this->raw[$offset]);
} }
@ -245,7 +250,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable
* *
* @param string $serialized * @param string $serialized
*/ */
public function unserialize($serialized)
public function unserialize($serialized): void
{ {
$this->raw = unserialize($serialized, null); $this->raw = unserialize($serialized, null);
} }

73
src/ShortsTrait.php

@ -14,30 +14,17 @@ namespace RouterOS;
trait ShortsTrait trait ShortsTrait
{ {
/** /**
* Alias for ->write() method
*
* @param string|array|\RouterOS\Query $query
* @return \RouterOS\Client
* @throws \RouterOS\Exceptions\QueryException
* @deprecated
*/
public function w($query): Client
{
return $this->write($query);
}
/**
* Alias for ->query() method * Alias for ->query() method
* *
* @param string $endpoint Path of API query
* @param array|string|\RouterOS\Interfaces\QueryInterface $endpoint Path of API query or Query object
* @param array|null $where List of where filters * @param array|null $where List of where filters
* @param string|null $operations Some operations which need make on response * @param string|null $operations Some operations which need make on response
* @param string|null $tag Mark query with tag * @param string|null $tag Mark query with tag
*
* @return \RouterOS\Client * @return \RouterOS\Client
* @throws \RouterOS\Exceptions\QueryException
* @since 1.0.0 * @since 1.0.0
*/ */
public function q(string $endpoint, array $where = null, string $operations = null, string $tag = null): Client
public function q($endpoint, array $where = null, string $operations = null, string $tag = null): Client
{ {
return $this->query($endpoint, $where, $operations, $tag); return $this->query($endpoint, $where, $operations, $tag);
} }
@ -45,7 +32,8 @@ trait ShortsTrait
/** /**
* Alias for ->read() method * Alias for ->read() method
* *
* @param bool $parse
* @param bool $parse If need parse output to array
*
* @return mixed * @return mixed
* @since 0.7 * @since 0.7
*/ */
@ -66,65 +54,34 @@ trait ShortsTrait
} }
/** /**
* Alias for ->write()->read() combination of methods
*
* @param string|array|\RouterOS\Query $query
* @param bool $parse
* @return array
* @throws \RouterOS\Exceptions\ClientException
* @throws \RouterOS\Exceptions\QueryException
* @since 0.6
* @deprecated
*/
public function wr($query, bool $parse = true): array
{
return $this->write($query)->read($parse);
}
/**
* Alias for ->write()->read() combination of methods
* Alias for ->query()->read() combination of methods
* *
* @param string $endpoint Path of API query
* @param array|string|\RouterOS\Interfaces\QueryInterface $endpoint Path of API query or Query object
* @param array|null $where List of where filters * @param array|null $where List of where filters
* @param string|null $operations Some operations which need make on response * @param string|null $operations Some operations which need make on response
* @param string|null $tag Mark query with tag * @param string|null $tag Mark query with tag
* @param bool $parse
* @return \RouterOS\Client
* @throws \RouterOS\Exceptions\QueryException
* @since 1.0.0
*/
public function qr(string $endpoint, array $where = null, string $operations = null, string $tag = null, bool $parse = true): array
{
return $this->query($endpoint, $where, $operations, $tag)->read($parse);
}
/**
* Alias for ->write()->readAsIterator() combination of methods
* @param bool $parse If need parse output to array
* *
* @param string|array|\RouterOS\Query $query
* @return \RouterOS\ResponseIterator
* @throws \RouterOS\Exceptions\ClientException
* @throws \RouterOS\Exceptions\QueryException
* @return array
* @since 1.0.0 * @since 1.0.0
* @deprecated
*/ */
public function wri($query): ResponseIterator
public function qr($endpoint, array $where = null, string $operations = null, string $tag = null, bool $parse = true): array
{ {
return $this->write($query)->readAsIterator();
return $this->query($endpoint, $where, $operations, $tag)->read($parse);
} }
/** /**
* Alias for ->write()->read() combination of methods
* Alias for ->query()->readAsIterator() combination of methods
* *
* @param string $endpoint Path of API query
* @param array|string|\RouterOS\Interfaces\QueryInterface $endpoint Path of API query or Query object
* @param array|null $where List of where filters * @param array|null $where List of where filters
* @param string|null $operations Some operations which need make on response * @param string|null $operations Some operations which need make on response
* @param string|null $tag Mark query with tag * @param string|null $tag Mark query with tag
*
* @return \RouterOS\ResponseIterator * @return \RouterOS\ResponseIterator
* @throws \RouterOS\Exceptions\QueryException
* @since 1.0.0 * @since 1.0.0
*/ */
public function qri(string $endpoint, array $where = null, string $operations = null, string $tag = null): ResponseIterator
public function qri($endpoint, array $where = null, string $operations = null, string $tag = null): ResponseIterator
{ {
return $this->query($endpoint, $where, $operations, $tag)->readAsIterator(); return $this->query($endpoint, $where, $operations, $tag)->readAsIterator();
} }

23
src/SocketTrait.php

@ -11,21 +11,21 @@ trait SocketTrait
* *
* @var resource|null * @var resource|null
*/ */
private $_socket;
private $socket;
/** /**
* Code of error * Code of error
* *
* @var int * @var int
*/ */
private $_socket_err_num;
private $socket_err_num;
/** /**
* Description of socket error * Description of socket error
* *
* @var string * @var string
*/ */
private $_socket_err_str;
private $socket_err_str;
/** /**
* Initiate socket session * Initiate socket session
@ -34,7 +34,7 @@ trait SocketTrait
* @throws \RouterOS\Exceptions\ClientException * @throws \RouterOS\Exceptions\ClientException
* @throws \RouterOS\Exceptions\ConfigException * @throws \RouterOS\Exceptions\ConfigException
*/ */
private function openSocket()
private function openSocket(): void
{ {
// Default: Context for ssl // Default: Context for ssl
$context = stream_context_create([ $context = stream_context_create([
@ -51,8 +51,8 @@ trait SocketTrait
// Initiate socket client // Initiate socket client
$socket = @stream_socket_client( $socket = @stream_socket_client(
$proto . $this->config('host') . ':' . $this->config('port'), $proto . $this->config('host') . ':' . $this->config('port'),
$this->_socket_err_num,
$this->_socket_err_str,
$this->socket_err_num,
$this->socket_err_str,
$this->config('timeout'), $this->config('timeout'),
STREAM_CLIENT_CONNECT, STREAM_CLIENT_CONNECT,
$context $context
@ -60,7 +60,7 @@ trait SocketTrait
// Throw error is socket is not initiated // Throw error is socket is not initiated
if (false === $socket) { if (false === $socket) {
throw new ClientException('Unable to establish socket session, ' . $this->_socket_err_str);
throw new ClientException('Unable to establish socket session, ' . $this->socket_err_str);
} }
//Timeout read //Timeout read
@ -77,18 +77,19 @@ trait SocketTrait
*/ */
private function closeSocket(): bool private function closeSocket(): bool
{ {
return fclose($this->_socket);
return fclose($this->socket);
} }
/** /**
* Save socket resource to static variable * Save socket resource to static variable
* *
* @param resource $socket * @param resource $socket
*
* @return void * @return void
*/ */
private function setSocket($socket)
private function setSocket($socket): void
{ {
$this->_socket = $socket;
$this->socket = $socket;
} }
/** /**
@ -98,6 +99,6 @@ trait SocketTrait
*/ */
public function getSocket() public function getSocket()
{ {
return $this->_socket;
return $this->socket;
} }
} }

27
src/Streams/ResourceStream.php

@ -38,10 +38,10 @@ class ResourceStream implements StreamInterface
} }
/** /**
* @param int $length
* @return string
* @throws \RouterOS\Exceptions\StreamException
* @throws \InvalidArgumentException
* @inheritDoc
*
* @throws \RouterOS\Exceptions\StreamException when length parameter is invalid
* @throws \InvalidArgumentException when the stream have been totally read and read method is called again
*/ */
public function read(int $length): string public function read(int $length): string
{ {
@ -60,17 +60,9 @@ class ResourceStream implements StreamInterface
} }
/** /**
* Writes a string to a stream
*
* Write $length bytes of string, if not mentioned, write all the string
* Must be binary safe (as fread).
* if $length is greater than string length, write all string and return number of writen bytes
* if $length os smaller than string length, remaining bytes are losts.
* @inheritDoc
* *
* @param string $string
* @param int|null $length the numer of bytes to read
* @return int the number of written bytes
* @throws \RouterOS\Exceptions\StreamException
* @throws \RouterOS\Exceptions\StreamException when not possible to write bytes
*/ */
public function write(string $string, int $length = null): int public function write(string $string, int $length = null): int
{ {
@ -89,12 +81,11 @@ class ResourceStream implements StreamInterface
} }
/** /**
* Close stream connection
* @inheritDoc
* *
* @return void
* @throws \RouterOS\Exceptions\StreamException
* @throws \RouterOS\Exceptions\StreamException when not possible to close the stream
*/ */
public function close()
public function close(): void
{ {
$hasBeenClosed = false; $hasBeenClosed = false;

22
src/Streams/StringStream.php

@ -2,6 +2,7 @@
namespace RouterOS\Streams; namespace RouterOS\Streams;
use InvalidArgumentException;
use RouterOS\Interfaces\StreamInterface; use RouterOS\Interfaces\StreamInterface;
use RouterOS\Exceptions\StreamException; use RouterOS\Exceptions\StreamException;
@ -31,18 +32,18 @@ class StringStream implements StreamInterface
$this->buffer = $string; $this->buffer = $string;
} }
/** /**
* {@inheritDoc}
* @inheritDoc
* *
* @throws \InvalidArgumentException when length parameter is invalid
* @throws StreamException when the stream have been tatly red and read methd is called again
* @throws \RouterOS\Exceptions\StreamException
*/ */
public function read(int $length): string public function read(int $length): string
{ {
$remaining = strlen($this->buffer); $remaining = strlen($this->buffer);
if ($length < 0) { if ($length < 0) {
throw new \InvalidArgumentException('Cannot read a negative count of bytes from a stream');
throw new InvalidArgumentException('Cannot read a negative count of bytes from a stream');
} }
if (0 === $remaining) { if (0 === $remaining) {
@ -65,11 +66,8 @@ class StringStream implements StreamInterface
} }
/** /**
* Fake write method, do nothing except return the "writen" length
* @inheritDoc
* *
* @param string $string The string to write
* @param int|null $length the number of characters to write
* @return int number of "writen" bytes
* @throws \InvalidArgumentException on invalid length * @throws \InvalidArgumentException on invalid length
*/ */
public function write(string $string, int $length = null): int public function write(string $string, int $length = null): int
@ -79,18 +77,16 @@ class StringStream implements StreamInterface
} }
if ($length < 0) { if ($length < 0) {
throw new \InvalidArgumentException('Cannot write a negative count of bytes');
throw new InvalidArgumentException('Cannot write a negative count of bytes');
} }
return min($length, strlen($string)); return min($length, strlen($string));
} }
/** /**
* Close stream connection
*
* @return void
* @inheritDoc
*/ */
public function close()
public function close(): void
{ {
$this->buffer = ''; $this->buffer = '';
} }

8
tests/APIConnectorTest.php

@ -25,7 +25,7 @@ class APIConnectorTest extends TestCase
* @param StreamInterface $stream Cannot typehint, PHP refuse it * @param StreamInterface $stream Cannot typehint, PHP refuse it
* @param bool $closeResource shall we close the resource ? * @param bool $closeResource shall we close the resource ?
*/ */
public function test_construct(StreamInterface $stream, bool $closeResource = false)
public function testConstruct(StreamInterface $stream, bool $closeResource = false): void
{ {
$apiStream = new APIConnector($stream); $apiStream = new APIConnector($stream);
$this->assertInstanceOf(APIConnector::class, $apiStream); $this->assertInstanceOf(APIConnector::class, $apiStream);
@ -38,7 +38,7 @@ class APIConnectorTest extends TestCase
{ {
return [ return [
[new ResourceStream(fopen(__FILE__, 'rb')),], // Myself, sure I exists [new ResourceStream(fopen(__FILE__, 'rb')),], // Myself, sure I exists
[new ResourceStream(fsockopen('tcp://' . getenv('ROS_HOST'), getenv('ROS_PORT_MODERN'))),], // Socket
[new ResourceStream(fsockopen('tcp://' . getenv('ROS_HOST'), getenv('ROS_PORT_MODERN')))], // Socket
[new ResourceStream(STDIN), false], // Try it, but do not close STDIN please !!! [new ResourceStream(STDIN), false], // Try it, but do not close STDIN please !!!
[new StringStream('Hello World !!!')], // Try it, but do not close STDIN please !!! [new StringStream('Hello World !!!')], // Try it, but do not close STDIN please !!!
[new StringStream('')], // Try it, but do not close STDIN please !!! [new StringStream('')], // Try it, but do not close STDIN please !!!
@ -53,7 +53,7 @@ class APIConnectorTest extends TestCase
* @param APIConnector $connector * @param APIConnector $connector
* @param string $expected * @param string $expected
*/ */
public function test__readWord(APIConnector $connector, string $expected)
public function testReadWord(APIConnector $connector, string $expected): void
{ {
$this->assertSame($expected, $connector->readWord()); $this->assertSame($expected, $connector->readWord());
} }
@ -79,7 +79,7 @@ class APIConnectorTest extends TestCase
* @param string $toWrite * @param string $toWrite
* @param int $expected * @param int $expected
*/ */
public function test_writeWord(APIConnector $connector, string $toWrite, int $expected)
public function testWriteWord(APIConnector $connector, string $toWrite, int $expected): void
{ {
$this->assertEquals($expected, $connector->writeWord($toWrite)); $this->assertEquals($expected, $connector->writeWord($toWrite));
} }

25
tests/APILengthCoDecTest.php

@ -2,9 +2,8 @@
namespace RouterOS\Tests; namespace RouterOS\Tests;
use DomainException;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Constraint\IsType;
use RouterOS\APILengthCoDec; use RouterOS\APILengthCoDec;
use RouterOS\Streams\StringStream; use RouterOS\Streams\StringStream;
use RouterOS\Helpers\BinaryStringHelper; use RouterOS\Helpers\BinaryStringHelper;
@ -18,11 +17,13 @@ class APILengthCoDecTest extends TestCase
{ {
/** /**
* @dataProvider encodeLengthNegativeProvider * @dataProvider encodeLengthNegativeProvider
* @expectedException \DomainException
* @covers ::encodeLength * @covers ::encodeLength
*
* @param $length
*/ */
public function test__encodeLengthNegative($length)
public function testEncodeLengthNegative($length): void
{ {
$this->expectException(DomainException::class);
APILengthCoDec::encodeLength($length); APILengthCoDec::encodeLength($length);
} }
@ -37,8 +38,11 @@ class APILengthCoDecTest extends TestCase
/** /**
* @dataProvider encodedLengthProvider * @dataProvider encodedLengthProvider
* @covers ::encodeLength * @covers ::encodeLength
*
* @param $expected
* @param $length
*/ */
public function test__encodeLength($expected, $length)
public function testEncodeLength($expected, $length): void
{ {
$this->assertEquals(BinaryStringHelper::IntegerToNBOBinaryString((int) $expected), APILengthCoDec::encodeLength($length)); $this->assertEquals(BinaryStringHelper::IntegerToNBOBinaryString((int) $expected), APILengthCoDec::encodeLength($length));
} }
@ -76,8 +80,11 @@ class APILengthCoDecTest extends TestCase
/** /**
* @dataProvider encodedLengthProvider * @dataProvider encodedLengthProvider
* @covers ::decodeLength * @covers ::decodeLength
*
* @param $encodedLength
* @param $expected
*/ */
public function test__decodeLength($encodedLength, $expected)
public function testDecodeLength($encodedLength, $expected): void
{ {
// We have to provide $encodedLength as a "bytes" stream // We have to provide $encodedLength as a "bytes" stream
$stream = new StringStream(BinaryStringHelper::IntegerToNBOBinaryString($encodedLength)); $stream = new StringStream(BinaryStringHelper::IntegerToNBOBinaryString($encodedLength));
@ -87,10 +94,12 @@ class APILengthCoDecTest extends TestCase
/** /**
* @dataProvider decodeLengthControlWordProvider * @dataProvider decodeLengthControlWordProvider
* @covers ::decodeLength * @covers ::decodeLength
* @expectedException \UnexpectedValueException
*
* @param string $encodedLength
*/ */
public function test_decodeLengthControlWord(string $encodedLength)
public function testDecodeLengthControlWord(string $encodedLength): void
{ {
$this->expectException(\UnexpectedValueException::class);
APILengthCoDec::decodeLength(new StringStream($encodedLength)); APILengthCoDec::decodeLength(new StringStream($encodedLength));
} }

224
tests/ClientTest.php

@ -2,6 +2,7 @@
namespace RouterOS\Tests; namespace RouterOS\Tests;
use Exception;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use RouterOS\Client; use RouterOS\Client;
use RouterOS\Exceptions\ConfigException; use RouterOS\Exceptions\ConfigException;
@ -15,7 +16,12 @@ class ClientTest extends TestCase
/** /**
* @var array * @var array
*/ */
public $router;
public $config;
/**
* @var \RouterOS\Client
*/
public $client;
/** /**
* @var int * @var int
@ -27,86 +33,100 @@ class ClientTest extends TestCase
*/ */
public $port_legacy; public $port_legacy;
public function setUp()
public function setUp(): void
{ {
parent::setUp();
$this->router = [
$this->config = [
'user' => getenv('ROS_USER'), 'user' => getenv('ROS_USER'),
'pass' => getenv('ROS_PASS'), 'pass' => getenv('ROS_PASS'),
'host' => getenv('ROS_HOST'), 'host' => getenv('ROS_HOST'),
'ssh_port' => (int) getenv('ROS_SSH_PORT'),
]; ];
$this->client = new Client($this->config);
$this->port_modern = (int) getenv('ROS_PORT_MODERN'); $this->port_modern = (int) getenv('ROS_PORT_MODERN');
$this->port_legacy = (int) getenv('ROS_PORT_LEGACY'); $this->port_legacy = (int) getenv('ROS_PORT_LEGACY');
} }
public function test__construct(): void
public function testConstruct(): void
{ {
try { try {
$config = new Config(); $config = new Config();
$config $config
->set('user', $this->router['user'])
->set('pass', $this->router['pass'])
->set('host', $this->router['host']);
->set('user', $this->config['user'])
->set('pass', $this->config['pass'])
->set('host', $this->config['host']);
$obj = new Client($config); $obj = new Client($config);
$this->assertIsObject($obj); $this->assertIsObject($obj);
$socket = $obj->getSocket(); $socket = $obj->getSocket();
$this->assertIsResource($socket); $this->assertIsResource($socket);
} catch (\Exception $e) {
$this->assertContains('Must be initialized ', $e->getMessage());
} catch (Exception $e) {
$this->assertStringContainsString('Must be initialized ', $e->getMessage());
} }
} }
public function test__construct2(): void
public function testConstruct2(): void
{ {
try { try {
$config = new Config($this->router);
$config = new Config($this->config);
$obj = new Client($config); $obj = new Client($config);
$this->assertIsObject($obj); $this->assertIsObject($obj);
$socket = $obj->getSocket(); $socket = $obj->getSocket();
$this->assertIsResource($socket); $this->assertIsResource($socket);
} catch (\Exception $e) {
$this->assertContains('Must be initialized ', $e->getMessage());
} catch (Exception $e) {
$this->assertStringContainsString('Must be initialized ', $e->getMessage());
} }
} }
public function test__construct3(): void
public function testConstruct3(): void
{ {
try { try {
$obj = new Client($this->router);
$obj = new Client($this->config);
$this->assertIsObject($obj); $this->assertIsObject($obj);
$socket = $obj->getSocket(); $socket = $obj->getSocket();
$this->assertIsResource($socket); $this->assertIsResource($socket);
} catch (\Exception $e) {
$this->assertContains('Must be initialized ', $e->getMessage());
} catch (Exception $e) {
$this->assertStringContainsString('Must be initialized ', $e->getMessage());
} }
} }
public function test__constructEx(): void
public function testConstructException(): void
{ {
$this->expectException(ConfigException::class); $this->expectException(ConfigException::class);
$obj = new Client([
'user' => $this->router['user'],
'pass' => $this->router['pass'],
new Client([
'user' => $this->config['user'],
'pass' => $this->config['pass'],
]); ]);
} }
public function test__constructLegacy(): void
public function testConstructExceptionBadHost(): void
{
$this->expectException(ClientException::class);
new Client([
'host' => '127.0.0.1',
'port' => 123456,
'attempts' => 0,
'user' => $this->config['user'],
'pass' => $this->config['pass'],
]);
}
public function testConstructLegacy(): void
{ {
try { try {
$obj = new Client([ $obj = new Client([
'user' => $this->router['user'],
'pass' => $this->router['pass'],
'host' => $this->router['host'],
'user' => $this->config['user'],
'pass' => $this->config['pass'],
'host' => $this->config['host'],
'port' => $this->port_legacy, 'port' => $this->port_legacy,
'legacy' => true 'legacy' => true
]); ]);
$this->assertIsObject($obj); $this->assertIsObject($obj);
} catch (\Exception $e) {
$this->assertContains('Must be initialized ', $e->getMessage());
} catch (Exception $e) {
$this->assertStringContainsString('Must be initialized ', $e->getMessage());
} }
} }
@ -115,46 +135,42 @@ class ClientTest extends TestCase
* *
* login() method recognise legacy router response and swap to legacy mode * login() method recognise legacy router response and swap to legacy mode
*/ */
public function test__constructLegacy2(): void
public function testConstructLegacy2(): void
{ {
try { try {
$obj = new Client([ $obj = new Client([
'user' => $this->router['user'],
'pass' => $this->router['pass'],
'host' => $this->router['host'],
'user' => $this->config['user'],
'pass' => $this->config['pass'],
'host' => $this->config['host'],
'port' => $this->port_legacy, 'port' => $this->port_legacy,
'legacy' => false 'legacy' => false
]); ]);
$this->assertIsObject($obj); $this->assertIsObject($obj);
} catch (\Exception $e) {
$this->assertContains('Must be initialized ', $e->getMessage());
} catch (Exception $e) {
$this->assertStringContainsString('Must be initialized ', $e->getMessage());
} }
} }
public function test__constructWrongPass(): void
public function testConstructWrongPass(): void
{ {
$this->expectException(ClientException::class); $this->expectException(ClientException::class);
$obj = new Client([
'user' => $this->router['user'],
new Client([
'user' => $this->config['user'],
'pass' => 'admin2', 'pass' => 'admin2',
'host' => $this->router['host'],
'host' => $this->config['host'],
'attempts' => 2 'attempts' => 2
]); ]);
} }
/**
* @expectedException ClientException
*/
public function test__constructWrongNet(): void
public function testConstructWrongNet(): void
{ {
$this->expectException(ClientException::class); $this->expectException(ClientException::class);
$obj = new Client([
'user' => $this->router['user'],
'pass' => $this->router['pass'],
'host' => $this->router['host'],
new Client([
'user' => $this->config['user'],
'pass' => $this->config['pass'],
'host' => $this->config['host'],
'port' => 11111, 'port' => 11111,
'attempts' => 2 'attempts' => 2
]); ]);
@ -162,41 +178,33 @@ class ClientTest extends TestCase
public function testQueryRead(): void public function testQueryRead(): void
{ {
$config = new Config();
$config
->set('user', $this->router['user'])
->set('pass', $this->router['pass'])
->set('host', $this->router['host']);
$obj = new Client($config);
/* /*
* Build query with where * Build query with where
*/ */
$read = $obj->query('/system/package/print', ['name'])->read();
$this->assertCount(13, $read);
$read = $this->client->query('/system/package/print', ['name'])->read();
$this->assertNotEmpty($read);
$read = $obj->query('/system/package/print', ['.id', '*1'])->read();
$read = $this->client->query('/system/package/print', ['.id', '*1'])->read();
$this->assertCount(1, $read); $this->assertCount(1, $read);
$read = $obj->query('/system/package/print', ['.id', '=', '*1'])->read();
$read = $this->client->query('/system/package/print', ['.id', '=', '*1'])->read();
$this->assertCount(1, $read); $this->assertCount(1, $read);
$read = $obj->query('/system/package/print', [['name']])->read();
$this->assertCount(13, $read);
$read = $this->client->query('/system/package/print', [['name']])->read();
$this->assertNotEmpty($read);
$read = $obj->query('/system/package/print', [['.id', '*1']])->read();
$read = $this->client->query('/system/package/print', [['.id', '*1']])->read();
$this->assertCount(1, $read); $this->assertCount(1, $read);
$read = $obj->query('/system/package/print', [['.id', '=', '*1']])->read();
$read = $this->client->query('/system/package/print', [['.id', '=', '*1']])->read();
$this->assertCount(1, $read); $this->assertCount(1, $read);
/* /*
* Build query with operations * Build query with operations
*/ */
$read = $obj->query('/interface/print', [
$read = $this->client->query('/interface/print', [
['type', 'ether'], ['type', 'ether'],
['type', 'vlan'] ['type', 'vlan']
], '|')->read(); ], '|')->read();
@ -207,68 +215,80 @@ class ClientTest extends TestCase
* Build query with tag * Build query with tag
*/ */
$read = $obj->query('/system/package/print', null, null, 'zzzz')->read();
$this->assertCount(13, $read);
$read = $this->client->query('/system/package/print', null, null, 'zzzz')->read();
// $this->assertCount(13, $read);
$this->assertEquals('zzzz', $read[0]['tag']); $this->assertEquals('zzzz', $read[0]['tag']);
} }
public function testReadAsIterator(): void public function testReadAsIterator(): void
{ {
$obj = new Client($this->router);
$obj = $obj->write('/system/package/print')->readAsIterator();
$this->assertIsObject($obj);
$result = $this->client->query('/system/package/print')->readAsIterator();
$this->assertIsObject($result);
} }
public function testWriteReadString(): void public function testWriteReadString(): void
{ {
$obj = new Client([
'user' => $this->router['user'],
'pass' => $this->router['pass'],
'host' => $this->router['host'],
]);
$readTrap = $obj->wr('/interface', false);
$readTrap = $this->client->query('/interface')->read(false);
$this->assertCount(3, $readTrap); $this->assertCount(3, $readTrap);
$this->assertEquals('!trap', $readTrap[0]); $this->assertEquals('!trap', $readTrap[0]);
} }
public function testFatal(): void public function testFatal(): void
{ {
$obj = new Client([
'user' => $this->router['user'],
'pass' => $this->router['pass'],
'host' => $this->router['host'],
]);
$readTrap = $obj->query('/quit')->read();
$readTrap = $this->client->query('/quit')->read();
$this->assertCount(2, $readTrap); $this->assertCount(2, $readTrap);
$this->assertEquals('!fatal', $readTrap[0]); $this->assertEquals('!fatal', $readTrap[0]);
} }
public function testQueryEx1(): void
public function queryExceptionDataProvider(): array
{ {
$this->expectException(ClientException::class);
$obj = new Client([
'user' => $this->router['user'],
'pass' => $this->router['pass'],
'host' => $this->router['host'],
]);
return [
// Wrong amount of parameters
['exception' => ClientException::class, 'endpoint' => '/quiet', 'attributes' => [[]]],
['exception' => ClientException::class, 'endpoint' => '/quiet', 'attributes' => [[], ['a', 'b', 'c']]],
['exception' => ClientException::class, 'endpoint' => '/quiet', 'attributes' => ['a', 'b', 'c', 'd']],
['exception' => ClientException::class, 'endpoint' => '/quiet', 'attributes' => [['a', 'b', 'c', 'd']]],
['exception' => ClientException::class, 'endpoint' => '/quiet', 'attributes' => [['a', 'b', 'c', 'd'], ['a', 'b', 'c']]],
// Wrong type of endpoint
['exception' => QueryException::class, 'endpoint' => 1, 'attributes' => null],
];
}
$obj->query('/quiet', ['a', 'b', 'c', 'd']);
/**
* @dataProvider queryExceptionDataProvider
*
* @param string $exception
* @param mixed $endpoint
* @param mixed $attributes
*
* @throws \RouterOS\Exceptions\ClientException
* @throws \RouterOS\Exceptions\ConfigException
* @throws \RouterOS\Exceptions\QueryException
*/
public function testQueryException(string $exception, $endpoint, $attributes): void
{
$this->expectException($exception);
$this->client->query($endpoint, $attributes);
} }
public function testQueryEx2(): void
public function testExportMethod(): void
{ {
$this->expectException(ClientException::class);
if (!in_array(gethostname(), ['pasha-lt', 'pasha-pc'])) {
$this->markTestSkipped('Travis does not allow to use SSH protocol on testing stage');
}
$obj = new Client([
'user' => $this->router['user'],
'pass' => $this->router['pass'],
'host' => $this->router['host'],
]);
$result = $this->client->export();
$this->assertNotEmpty($result);
}
public function testExportQuery(): void
{
if (!in_array(gethostname(), ['pasha-lt', 'pasha-pc'])) {
$this->markTestSkipped('Travis does not allow to use SSH protocol on testing stage');
}
$obj->query('/quiet', [[]]);
$result = $this->client->query('/export');
$this->assertNotEmpty($result);
} }
} }

55
tests/ConfigTest.php

@ -8,58 +8,58 @@ use RouterOS\Exceptions\ConfigException;
class ConfigTest extends TestCase class ConfigTest extends TestCase
{ {
public function test__construct()
public function testConstruct(): void
{ {
try { try {
$obj = new Config(); $obj = new Config();
$this->assertInternalType('object', $obj);
$this->assertIsObject($obj);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->assertContains('Must be initialized ', $e->getMessage());
$this->assertStringContainsString('Must be initialized ', $e->getMessage());
} }
} }
public function testGetParameters()
public function testGetParameters(): void
{ {
$obj = new Config(); $obj = new Config();
$params = $obj->getParameters(); $params = $obj->getParameters();
$this->assertCount(5, $params);
$this->assertEquals($params['legacy'], false);
$this->assertEquals($params['ssl'], false);
$this->assertEquals($params['timeout'], 10);
$this->assertEquals($params['attempts'], 10);
$this->assertEquals($params['delay'], 1);
$this->assertCount(6, $params);
$this->assertEquals(false, $params['legacy']);
$this->assertEquals(false, $params['ssl']);
$this->assertEquals(10, $params['timeout']);
$this->assertEquals(10, $params['attempts']);
$this->assertEquals(1, $params['delay']);
} }
public function testGetParameters2()
public function testGetParameters2(): void
{ {
$obj = new Config(['timeout' => 100]); $obj = new Config(['timeout' => 100]);
$params = $obj->getParameters(); $params = $obj->getParameters();
$this->assertCount(5, $params);
$this->assertEquals($params['timeout'], 100);
$this->assertCount(6, $params);
$this->assertEquals(100, $params['timeout']);
} }
public function testSet()
public function testSet(): void
{ {
$obj = new Config(); $obj = new Config();
$obj->set('timeout', 111); $obj->set('timeout', 111);
$params = $obj->getParameters(); $params = $obj->getParameters();
$this->assertEquals($params['timeout'], 111);
$this->assertEquals(111, $params['timeout']);
} }
public function testSetArr()
public function testSetArr(): void
{ {
$obj = new Config([ $obj = new Config([
'timeout' => 111 'timeout' => 111
]); ]);
$params = $obj->getParameters(); $params = $obj->getParameters();
$this->assertEquals($params['timeout'], 111);
$this->assertEquals(111, $params['timeout']);
} }
public function testDelete()
public function testDelete(): void
{ {
$obj = new Config(); $obj = new Config();
$obj->delete('timeout'); $obj->delete('timeout');
@ -68,7 +68,7 @@ class ConfigTest extends TestCase
$this->assertArrayNotHasKey('timeout', $params); $this->assertArrayNotHasKey('timeout', $params);
} }
public function testDeleteEx()
public function testDeleteEx(): void
{ {
$this->expectException(ConfigException::class); $this->expectException(ConfigException::class);
@ -76,7 +76,7 @@ class ConfigTest extends TestCase
$obj->delete('wrong'); $obj->delete('wrong');
} }
public function testSetEx1()
public function testSetExceptionWrongType(): void
{ {
$this->expectException(ConfigException::class); $this->expectException(ConfigException::class);
@ -84,7 +84,7 @@ class ConfigTest extends TestCase
$obj->set('delay', 'some string'); $obj->set('delay', 'some string');
} }
public function testSetEx2()
public function testSetExceptionWrongKey(): void
{ {
$this->expectException(ConfigException::class); $this->expectException(ConfigException::class);
@ -92,27 +92,26 @@ class ConfigTest extends TestCase
$obj->set('wrong', 'some string'); $obj->set('wrong', 'some string');
} }
public function testGet()
public function testGet(): void
{ {
$obj = new Config(); $obj = new Config();
$test1 = $obj->get('legacy'); $test1 = $obj->get('legacy');
$this->assertEquals($test1, false);
$this->assertEquals(false, $test1);
$test2 = $obj->get('port'); $test2 = $obj->get('port');
$this->assertEquals($test2, 8728);
$this->assertEquals(8728, $test2);
$obj->set('port', 10000); $obj->set('port', 10000);
$test3 = $obj->get('port'); $test3 = $obj->get('port');
$this->assertEquals($test3, 10000);
$this->assertEquals(10000, $test3);
$obj->delete('port'); $obj->delete('port');
$obj->set('ssl', true); $obj->set('ssl', true);
$test3 = $obj->get('port'); $test3 = $obj->get('port');
$this->assertEquals($test3, 8729);
$this->assertEquals(8729, $test3);
} }
public function testGetEx()
public function testGetEx(): void
{ {
$this->expectException(ConfigException::class); $this->expectException(ConfigException::class);

4
tests/Helpers/ArrayHelperTest.php

@ -7,7 +7,7 @@ use RouterOS\Helpers\ArrayHelper;
class ArrayHelperTest extends TestCase class ArrayHelperTest extends TestCase
{ {
public function testCheckIfKeyNotExist()
public function testCheckIfKeyNotExist(): void
{ {
$test1 = ArrayHelper::checkIfKeyNotExist(1, [0 => 'a', 1 => 'b', 2 => 'c']); $test1 = ArrayHelper::checkIfKeyNotExist(1, [0 => 'a', 1 => 'b', 2 => 'c']);
$this->assertFalse($test1); $this->assertFalse($test1);
@ -16,7 +16,7 @@ class ArrayHelperTest extends TestCase
$this->assertTrue($test2); $this->assertTrue($test2);
} }
public function testCheckIfKeysNotExist()
public function testCheckIfKeysNotExist(): void
{ {
$test1 = ArrayHelper::checkIfKeysNotExist([1, 2], [0 => 'a', 1 => 'b', 2 => 'c']); $test1 = ArrayHelper::checkIfKeysNotExist([1, 2], [0 => 'a', 1 => 'b', 2 => 'c']);
$this->assertTrue($test1); $this->assertTrue($test1);

5
tests/Helpers/BinaryStringHelperTest.php

@ -16,8 +16,11 @@ class BinaryStringHelperTest extends TestCase
/** /**
* @dataProvider IntegerToNBOBinaryStringProvider * @dataProvider IntegerToNBOBinaryStringProvider
* @covers ::IntegerToNBOBinaryString * @covers ::IntegerToNBOBinaryString
*
* @param $value
* @param $expected
*/ */
public function test__IntegerToNBOBinaryString($value, $expected)
public function testIntegerToNBOBinaryString($value, $expected): void
{ {
$this->assertEquals($expected, BinaryStringHelper::IntegerToNBOBinaryString($value)); $this->assertEquals($expected, BinaryStringHelper::IntegerToNBOBinaryString($value));
} }

2
tests/Helpers/TypeHelperTest.php

@ -7,7 +7,7 @@ use RouterOS\Helpers\TypeHelper;
class TypeHelperTest extends TestCase class TypeHelperTest extends TestCase
{ {
public function testCheckIfTypeMismatch()
public function testCheckIfTypeMismatch(): void
{ {
$test1 = TypeHelper::checkIfTypeMismatch(gettype(true), gettype(false)); $test1 = TypeHelper::checkIfTypeMismatch(gettype(true), gettype(false));
$this->assertFalse($test1); $this->assertFalse($test1);

62
tests/Laravel/ServiceProviderTests.php

@ -0,0 +1,62 @@
<?php
namespace RouterOS\Tests\Laravel;
use RouterOS\Config;
use RouterOS\Laravel\Wrapper;
class ServiceProviderTests extends TestCase
{
private $client = [
"__construct",
"query",
"read",
"readAsIterator",
"parseResponse",
"connect",
"export",
"getSocket",
"q",
"r",
"ri",
"qr",
"qri",
];
public function testAbstractsAreLoaded(): void
{
$manager = app(Wrapper::class);
$this->assertInstanceOf(Wrapper::class, $manager);
}
public function testConfig(): void
{
$config = \RouterOS::config([
'host' => '192.168.1.3',
'user' => 'admin',
'pass' => 'admin'
]);
$this->assertInstanceOf(Config::class, $config);
$params = $config->getParameters();
$this->assertArrayHasKey('host', $params);
$this->assertArrayHasKey('user', $params);
$this->assertArrayHasKey('pass', $params);
$this->assertArrayHasKey('ssl', $params);
$this->assertArrayHasKey('legacy', $params);
$this->assertArrayHasKey('timeout', $params);
$this->assertArrayHasKey('attempts', $params);
$this->assertArrayHasKey('delay', $params);
}
public function testClient(): void
{
$client = \RouterOS::client([
'host' => '192.168.1.3',
'user' => 'admin',
'pass' => 'admin'
], false);
$this->assertEquals(get_class_methods($client), $this->client);
}
}

35
tests/Laravel/TestCase.php

@ -0,0 +1,35 @@
<?php
namespace RouterOS\Tests\Laravel;
use RouterOS\Laravel\Facade;
use RouterOS\Laravel\ServiceProvider;
use Orchestra\Testbench\TestCase as Orchestra;
/**
* Class TestCase
*
* @package Tests
*/
abstract class TestCase extends Orchestra
{
/**
* @inheritdoc
*/
protected function getPackageProviders($app): array
{
return [
ServiceProvider::class,
];
}
/**
* @inheritdoc
*/
protected function getPackageAliases($app): array
{
return [
'RouterOS' => Facade::class,
];
}
}

30
tests/QueryTest.php

@ -8,33 +8,33 @@ use RouterOS\Query;
class QueryTest extends TestCase class QueryTest extends TestCase
{ {
public function test__construct(): void
public function testConstruct(): void
{ {
try { try {
$obj = new Query('test'); $obj = new Query('test');
$this->assertIsObject($obj); $this->assertIsObject($obj);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->assertContains('Must be initialized ', $e->getMessage());
$this->assertStringContainsString('Must be initialized ', $e->getMessage());
} }
} }
public function test__construct_arr(): void
public function testConstructArr(): void
{ {
try { try {
$obj = new Query('test', ['line1', 'line2', 'line3']); $obj = new Query('test', ['line1', 'line2', 'line3']);
$this->assertIsObject($obj); $this->assertIsObject($obj);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->assertContains('Must be initialized ', $e->getMessage());
$this->assertStringContainsString('Must be initialized ', $e->getMessage());
} }
} }
public function test__construct_arr2(): void
public function testConstructArr2(): void
{ {
try { try {
$obj = new Query(['test', 'line1', 'line2', 'line3']); $obj = new Query(['test', 'line1', 'line2', 'line3']);
$this->assertIsObject($obj); $this->assertIsObject($obj);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->assertContains('Must be initialized ', $e->getMessage());
$this->assertStringContainsString('Must be initialized ', $e->getMessage());
} }
} }
@ -42,14 +42,14 @@ class QueryTest extends TestCase
{ {
$obj = new Query('test'); $obj = new Query('test');
$test = $obj->getEndpoint(); $test = $obj->getEndpoint();
$this->assertEquals($test, 'test');
$this->assertEquals('test', $test);
} }
public function testGetEndpoint2(): void public function testGetEndpoint2(): void
{ {
$obj = new Query(['zzz', 'line1', 'line2', 'line3']); $obj = new Query(['zzz', 'line1', 'line2', 'line3']);
$test = $obj->getEndpoint(); $test = $obj->getEndpoint();
$this->assertEquals($test, 'zzz');
$this->assertEquals('zzz', $test);
} }
public function testGetEndpointEx(): void public function testGetEndpointEx(): void
@ -65,7 +65,7 @@ class QueryTest extends TestCase
$obj = new Query('test'); $obj = new Query('test');
$obj->setEndpoint('zzz'); $obj->setEndpoint('zzz');
$test = $obj->getEndpoint(); $test = $obj->getEndpoint();
$this->assertEquals($test, 'zzz');
$this->assertEquals('zzz', $test);
} }
public function testGetAttributes(): void public function testGetAttributes(): void
@ -104,6 +104,18 @@ class QueryTest extends TestCase
$this->assertEquals($attrs[1], '?key2=value2'); $this->assertEquals($attrs[1], '?key2=value2');
} }
public function testEqual(): void
{
$obj = new Query('test');
$obj->equal('key1', 'value1');
$obj->equal('key2', 'value2');
$attrs = $obj->getAttributes();
$this->assertCount(2, $attrs);
$this->assertEquals($attrs[1], '=key2=value2');
}
public function testTag(): void public function testTag(): void
{ {
$obj = new Query('/test/test'); $obj = new Query('/test/test');

39
tests/ResponseIteratorTest.php

@ -4,38 +4,34 @@ namespace RouterOS\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use RouterOS\Client; use RouterOS\Client;
use RouterOS\ResponseIterator;
class ResponseIteratorTest extends TestCase class ResponseIteratorTest extends TestCase
{ {
public function test__construct()
/**
* @var \RouterOS\Client
*/
private $client;
public function setUp(): void
{ {
$obj = new Client([
$this->client = new Client([
'user' => getenv('ROS_USER'), 'user' => getenv('ROS_USER'),
'pass' => getenv('ROS_PASS'), 'pass' => getenv('ROS_PASS'),
'host' => getenv('ROS_HOST'), 'host' => getenv('ROS_HOST'),
]); ]);
$obj = $obj->write('/system/package/print')->readAsIterator();
$this->assertIsObject($obj);
} }
public function testReadWrite()
public function testReadWrite(): void
{ {
$obj = new Client([
'user' => getenv('ROS_USER'),
'pass' => getenv('ROS_PASS'),
'host' => getenv('ROS_HOST'),
]);
$readTrap = $obj->write('/system/package/print')->readAsIterator();
// Read from RAW
$this->assertCount(13, $readTrap);
$readTrap = $this->client->query('/system/logging/print')->readAsIterator();
$this->assertNotEmpty($readTrap);
$readTrap = $obj->write('/ip/address/print')->readAsIterator();
$readTrap = $this->client->query('/ip/address/print')->readAsIterator();
$this->assertCount(1, $readTrap); $this->assertCount(1, $readTrap);
$this->assertEquals('ether1', $readTrap[0]['interface']); $this->assertEquals('ether1', $readTrap[0]['interface']);
$readTrap = $obj->write('/system/package/print')->readAsIterator();
$readTrap = $this->client->query('/system/logging/print')->readAsIterator();
$key = $readTrap->key(); $key = $readTrap->key();
$this->assertEquals(0, $key); $this->assertEquals(0, $key);
$current = $readTrap->current(); $current = $readTrap->current();
@ -62,14 +58,9 @@ class ResponseIteratorTest extends TestCase
public function testSerialize(): void public function testSerialize(): void
{ {
$obj = new Client([
'user' => getenv('ROS_USER'),
'pass' => getenv('ROS_PASS'),
'host' => getenv('ROS_HOST'),
]);
$read = $obj->write('/queue/simple/print')->readAsIterator();
$read = $this->client->query('/queue/simple/print')->readAsIterator();
$serialize = $read->serialize(); $serialize = $read->serialize();
$this->assertEquals('a:1:{i:0;a:1:{i:0;s:5:"!done";}}', $serialize); $this->assertEquals('a:1:{i:0;a:1:{i:0;s:5:"!done";}}', $serialize);
} }

46
tests/Streams/ResourceStreamTest.php

@ -2,8 +2,10 @@
namespace RouterOS\Tests\Streams; namespace RouterOS\Tests\Streams;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Constraint\IsType; use PHPUnit\Framework\Constraint\IsType;
use RouterOS\Exceptions\StreamException;
use RouterOS\Streams\ResourceStream; use RouterOS\Streams\ResourceStream;
/** /**
@ -17,14 +19,14 @@ class ResourceStreamTest extends TestCase
* Test that constructor throws an InvalidArgumentException on bad parameter type * Test that constructor throws an InvalidArgumentException on bad parameter type
* *
* @covers ::__construct * @covers ::__construct
* @expectedException \InvalidArgumentException
* @dataProvider constructNotResourceProvider * @dataProvider constructNotResourceProvider
* *
* @param $notResource * @param $notResource
*/ */
public function test__constructNotResource($notResource)
public function testConstructNotResource($notResource): void
{ {
$this->expectException(InvalidArgumentException::class);
new ResourceStream($notResource); new ResourceStream($notResource);
} }
@ -56,12 +58,17 @@ class ResourceStreamTest extends TestCase
* @param resource $resource Cannot typehint, PHP refuse it * @param resource $resource Cannot typehint, PHP refuse it
* @param bool $closeResource shall we close the resource ? * @param bool $closeResource shall we close the resource ?
*/ */
public function test_construct($resource, bool $closeResource = true)
public function testConstruct($resource, bool $closeResource = true): void
{ {
$resourceStream = new ResourceStream($resource);
$resourceStream = new class($resource) extends ResourceStream {
public function getStream()
{
return $this->stream;
}
};
$stream = $this->getObjectAttribute($resourceStream, 'stream');
$this->assertInternalType(IsType::TYPE_RESOURCE, $stream);
$stream = $resourceStream->getStream();
$this->assertIsResource($stream);
if ($closeResource) { if ($closeResource) {
fclose($resource); fclose($resource);
@ -91,10 +98,11 @@ class ResourceStreamTest extends TestCase
* *
* @param ResourceStream $stream Cannot typehint, PHP refuse it * @param ResourceStream $stream Cannot typehint, PHP refuse it
* @param string $expected the result we should have * @param string $expected the result we should have
*
* @throws \RouterOS\Exceptions\StreamException * @throws \RouterOS\Exceptions\StreamException
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function test__read(ResourceStream $stream, string $expected)
public function testRead(ResourceStream $stream, string $expected): void
{ {
$this->assertSame($expected, $stream->read(strlen($expected))); $this->assertSame($expected, $stream->read(strlen($expected)));
} }
@ -115,15 +123,16 @@ class ResourceStreamTest extends TestCase
* *
* @covers ::read * @covers ::read
* @dataProvider readBadLengthProvider * @dataProvider readBadLengthProvider
* @expectedException \InvalidArgumentException
* *
* @param ResourceStream $stream Cannot typehint, PHP refuse it * @param ResourceStream $stream Cannot typehint, PHP refuse it
* @param int $length * @param int $length
*
* @throws \RouterOS\Exceptions\StreamException * @throws \RouterOS\Exceptions\StreamException
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function test__readBadLength(ResourceStream $stream, int $length)
public function testReadBadLength(ResourceStream $stream, int $length): void
{ {
$this->expectException(InvalidArgumentException::class);
$stream->read($length); $stream->read($length);
} }
@ -143,13 +152,13 @@ class ResourceStreamTest extends TestCase
* *
* @covers ::read * @covers ::read
* @dataProvider readBadResourceProvider * @dataProvider readBadResourceProvider
* @expectedException \RouterOS\Exceptions\StreamException
* *
* @param ResourceStream $stream Cannot typehint, PHP refuse it * @param ResourceStream $stream Cannot typehint, PHP refuse it
* @param int $length * @param int $length
*/ */
public function test__readBadResource(ResourceStream $stream, int $length)
public function testReadBadResource(ResourceStream $stream, int $length): void
{ {
$this->expectException(StreamException::class);
$stream->read($length); $stream->read($length);
} }
@ -171,9 +180,10 @@ class ResourceStreamTest extends TestCase
* *
* @param ResourceStream $stream to test * @param ResourceStream $stream to test
* @param string $toWrite the writed string * @param string $toWrite the writed string
*
* @throws \RouterOS\Exceptions\StreamException * @throws \RouterOS\Exceptions\StreamException
*/ */
public function test__write(ResourceStream $stream, string $toWrite)
public function testWrite(ResourceStream $stream, string $toWrite): void
{ {
$this->assertEquals(strlen($toWrite), $stream->write($toWrite)); $this->assertEquals(strlen($toWrite), $stream->write($toWrite));
} }
@ -193,13 +203,13 @@ class ResourceStreamTest extends TestCase
* *
* @covers ::write * @covers ::write
* @dataProvider writeBadResourceProvider * @dataProvider writeBadResourceProvider
* @expectedException \RouterOS\Exceptions\StreamException
* *
* @param ResourceStream $stream to test * @param ResourceStream $stream to test
* @param string $toWrite the written string * @param string $toWrite the written string
*/ */
public function test__writeBadResource(ResourceStream $stream, string $toWrite)
public function testWriteBadResource(ResourceStream $stream, string $toWrite): void
{ {
$this->expectException(StreamException::class);
$stream->write($toWrite); $stream->write($toWrite);
} }
@ -219,12 +229,12 @@ class ResourceStreamTest extends TestCase
* *
* @covers ::close * @covers ::close
* @dataProvider doubleCloseProvider * @dataProvider doubleCloseProvider
* @expectedException \RouterOS\Exceptions\StreamException
* *
* @param ResourceStream $stream to test * @param ResourceStream $stream to test
*/ */
public function test_doubleClose(ResourceStream $stream)
public function testDoubleClose(ResourceStream $stream): void
{ {
$this->expectException(StreamException::class);
$stream->close(); $stream->close();
$stream->close(); $stream->close();
} }
@ -242,13 +252,13 @@ class ResourceStreamTest extends TestCase
* @covers ::close * @covers ::close
* @covers ::write * @covers ::write
* @dataProvider writeClosedResourceProvider * @dataProvider writeClosedResourceProvider
* @expectedException \RouterOS\Exceptions\StreamException
* *
* @param ResourceStream $stream to test * @param ResourceStream $stream to test
* @param string $toWrite the written string * @param string $toWrite the written string
*/ */
public function test_close(ResourceStream $stream, string $toWrite)
public function testClose(ResourceStream $stream, string $toWrite)
{ {
$this->expectException(StreamException::class);
$stream->close(); $stream->close();
$stream->write($toWrite); $stream->write($toWrite);
} }

29
tests/Streams/StringStreamTest.php

@ -21,7 +21,7 @@ class StringStreamTest extends TestCase
* *
* @param string $string * @param string $string
*/ */
public function test__construct(string $string)
public function testConstruct(string $string): void
{ {
$this->assertInstanceOf(StringStream::class, new StringStream($string)); $this->assertInstanceOf(StringStream::class, new StringStream($string));
} }
@ -36,7 +36,6 @@ class StringStreamTest extends TestCase
]; ];
} }
/** /**
* Test that write function returns the effective written bytes * Test that write function returns the effective written bytes
* *
@ -48,7 +47,7 @@ class StringStreamTest extends TestCase
* @param int $expected the number of bytes that must be writen * @param int $expected the number of bytes that must be writen
*/ */
public function test__write(string $string, $length, int $expected)
public function testWrite(string $string, $length, int $expected): void
{ {
$stream = new StringStream('Does not matters'); $stream = new StringStream('Does not matters');
if (null === $length) { if (null === $length) {
@ -79,10 +78,10 @@ class StringStreamTest extends TestCase
/** /**
* @covers ::write * @covers ::write
* @expectedException \InvalidArgumentException
*/ */
public function test__writeWithNegativeLength()
public function testWriteWithNegativeLength(): void
{ {
$this->expectException(\InvalidArgumentException::class);
$stream = new StringStream('Does not matters'); $stream = new StringStream('Does not matters');
$stream->write('PLOP', -1); $stream->write('PLOP', -1);
} }
@ -92,7 +91,7 @@ class StringStreamTest extends TestCase
* *
* @throws \RouterOS\Exceptions\StreamException * @throws \RouterOS\Exceptions\StreamException
*/ */
public function test__read()
public function testRead(): void
{ {
$stream = new StringStream('123456789'); $stream = new StringStream('123456789');
@ -105,12 +104,11 @@ class StringStreamTest extends TestCase
} }
/** /**
* @expectedException \InvalidArgumentException
*
* @throws \RouterOS\Exceptions\StreamException * @throws \RouterOS\Exceptions\StreamException
*/ */
public function test__readBadLength()
public function testReadBadLength(): void
{ {
$this->expectException(\InvalidArgumentException::class);
$stream = new StringStream('123456789'); $stream = new StringStream('123456789');
$stream->read(-1); $stream->read(-1);
} }
@ -118,14 +116,15 @@ class StringStreamTest extends TestCase
/** /**
* @covers ::read * @covers ::read
* @dataProvider readWhileEmptyProvider * @dataProvider readWhileEmptyProvider
* @expectedException \RouterOS\Exceptions\StreamException
* *
* @param StringStream $stream * @param StringStream $stream
* @param int $length * @param int $length
*
* @throws \RouterOS\Exceptions\StreamException * @throws \RouterOS\Exceptions\StreamException
*/ */
public function test__readWhileEmpty(StringStream $stream, int $length)
public function testReadWhileEmpty(StringStream $stream, int $length): void
{ {
$this->expectException(\RouterOS\Exceptions\StreamException::class);
$stream->read($length); $stream->read($length);
} }
@ -133,7 +132,7 @@ class StringStreamTest extends TestCase
* @return \Generator * @return \Generator
* @throws StreamException * @throws StreamException
*/ */
public function readWhileEmptyProvider()
public function readWhileEmptyProvider(): ?\Generator
{ {
$stream = new StringStream('123456789'); $stream = new StringStream('123456789');
$stream->read(9); $stream->read(9);
@ -148,11 +147,9 @@ class StringStreamTest extends TestCase
yield [$stream, 1]; yield [$stream, 1];
} }
/**
* @expectedException \RouterOS\Exceptions\StreamException
*/
public function testReadClosed()
public function testReadClosed(): void
{ {
$this->expectException(\RouterOS\Exceptions\StreamException::class);
$stream = new StringStream('123456789'); $stream = new StringStream('123456789');
$stream->close(); $stream->close();
$stream->read(1); $stream->read(1);

Loading…
Cancel
Save