Add unit tests
This commit is contained in:
parent
7cd237485a
commit
1cbe008a86
|
|
@ -4,3 +4,6 @@
|
||||||
|
|
||||||
# Composer files:
|
# Composer files:
|
||||||
/vendor/
|
/vendor/
|
||||||
|
|
||||||
|
# PHPUnit files:
|
||||||
|
.phpunit.result.cache
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,4 @@ $config = new Config(__DIR__ . "/../config/pb-dydns.json");
|
||||||
$logger = new Logger(__DIR__ . "/../logs/pb-dydns.log");
|
$logger = new Logger(__DIR__ . "/../logs/pb-dydns.log");
|
||||||
|
|
||||||
$cmd = new UpdateDnsCommand($config, $logger);
|
$cmd = new UpdateDnsCommand($config, $logger);
|
||||||
$cmd->run($argv);
|
exit($cmd->run($argv));
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,8 @@
|
||||||
"App\\": "src/"
|
"App\\": "src/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require": {}
|
"require-dev":
|
||||||
|
{
|
||||||
|
"phpunit/phpunit": "^12.5"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit bootstrap="vendor/autoload.php" colors="true" verbose="true" backupGlobals="false" backupStaticAttributes="false">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="pb-dydns Test Suite">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
</phpunit>
|
||||||
|
|
@ -9,6 +9,10 @@ use App\Util\Console;
|
||||||
|
|
||||||
class UpdateDnsCommand
|
class UpdateDnsCommand
|
||||||
{
|
{
|
||||||
|
public const STATUS_NOTHING_TO_CHANGE = 0;
|
||||||
|
public const STATUS_UPDATED = 1;
|
||||||
|
public const STATUS_ERROR = 2;
|
||||||
|
|
||||||
private Config $config;
|
private Config $config;
|
||||||
private Logger $logger;
|
private Logger $logger;
|
||||||
|
|
||||||
|
|
@ -18,61 +22,69 @@ class UpdateDnsCommand
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(array $argv): void
|
public function run(array $argv, ?PorkbunAPI $api = null, ?string $myIp = null): int
|
||||||
{
|
{
|
||||||
if (count($argv) < 2)
|
if (count($argv) < 2)
|
||||||
{
|
{
|
||||||
echo "Usage: pb-dydns.php <domain>\n";
|
echo "Usage: pb-dydns.php <domain>\n";
|
||||||
exit(1);
|
return self::STATUS_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
$domain = $argv[1];
|
$domain = $argv[1];
|
||||||
|
|
||||||
$api = new PorkbunAPI($this->config->get('config_filename'));
|
// Allow API injection for tests:
|
||||||
|
$api = $api ?? new PorkbunAPI($this->config->get('config_filename'));
|
||||||
|
|
||||||
// Test connection to Porkbun API in order to get the public IP:
|
// 1. Test connection to Porkbun API in order to get the public IP:
|
||||||
$raw = $api->ping();
|
$raw = $api->ping();
|
||||||
$result = json_decode($raw);
|
$result = json_decode($raw);
|
||||||
|
|
||||||
if ($result === null)
|
if ($result === null)
|
||||||
{
|
{
|
||||||
Console::echo("Invalid JSON returned by Porkbun API:\n$raw\n");
|
Console::echo("Invalid JSON returned by Porkbun API:\n$raw\n");
|
||||||
exit(3);
|
return self::STATUS_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($result->status))
|
if (!isset($result->status))
|
||||||
{
|
{
|
||||||
Console::echo("Porkbun API response missing 'status' field:\n$raw\n");
|
Console::echo("Porkbun API response missing 'status' field:\n$raw\n");
|
||||||
exit(3);
|
return self::STATUS_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($result->status !== "SUCCESS")
|
if ($result->status !== "SUCCESS")
|
||||||
{
|
{
|
||||||
Console::echo("Porkbun API returned an error:\n$raw\n");
|
Console::echo("Porkbun API returned an error:\n$raw\n");
|
||||||
exit(3);
|
return self::STATUS_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($result->yourIp))
|
if (!isset($result->yourIp))
|
||||||
{
|
{
|
||||||
Console::echo("Porkbun API did not return your public IP.\n");
|
Console::echo("Porkbun API did not return your public IP.\n");
|
||||||
exit(3);
|
return self::STATUS_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
$myIp = $result->yourIp;
|
$myIp = $myIp ?? $result->yourIp;
|
||||||
|
|
||||||
Console::echo("Your public IP address is $myIp" . PHP_EOL);
|
Console::echo("Your public IP address is $myIp" . PHP_EOL);
|
||||||
|
|
||||||
// Retrieve all DNS records associated with user's domain:
|
// 2. Retrieve all DNS records associated with user's domain:
|
||||||
$records = $api->retrieve($domain);
|
$records = $api->retrieve($domain);
|
||||||
|
$data = json_decode($records);
|
||||||
|
|
||||||
|
if ($data === null || !isset($data->records))
|
||||||
|
{
|
||||||
|
Console::echo("Invalid DNS records returned by Porkbun API:\n$records\n");
|
||||||
|
return self::STATUS_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
// Discard those records that are not type "A":
|
// Discard those records that are not type "A":
|
||||||
$data = json_decode($records);
|
|
||||||
$filteredRecords = array_filter($data->records, function ($record)
|
$filteredRecords = array_filter($data->records, function ($record)
|
||||||
{
|
{
|
||||||
return $record->type == "A";
|
return isset($record->type) && $record->type == "A";
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the records that passed the filter with the public IP:
|
// 3. Update the records that passed the filter with the public IP:
|
||||||
|
$updated = false;
|
||||||
foreach ($filteredRecords as $record)
|
foreach ($filteredRecords as $record)
|
||||||
{
|
{
|
||||||
Console::echo("Porkbun's DNS for $record->name is pointing to $record->content... ");
|
Console::echo("Porkbun's DNS for $record->name is pointing to $record->content... ");
|
||||||
|
|
@ -84,12 +96,17 @@ class UpdateDnsCommand
|
||||||
$name = rtrim(strstr($record->name, $domain, true), ".");
|
$name = rtrim(strstr($record->name, $domain, true), ".");
|
||||||
$result = json_decode($api->edit($domain, $record->id, $myIp, $name));
|
$result = json_decode($api->edit($domain, $record->id, $myIp, $name));
|
||||||
|
|
||||||
if ($result->status == "SUCCESS")
|
if ($result && isset($result->status) && $result->status === "SUCCESS")
|
||||||
{
|
{
|
||||||
Console::echo("Done!" . PHP_EOL);
|
Console::echo("Done!\n");
|
||||||
|
$this->logger->log("Updated DNS on $record->name from $record->content to $myIp");
|
||||||
|
$updated = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console::echo("Failed!\n");
|
||||||
|
return self::STATUS_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->logger->log("Updated DNS on $record->name from $record->content to $myIp");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
@ -99,5 +116,7 @@ class UpdateDnsCommand
|
||||||
|
|
||||||
Console::echo("");
|
Console::echo("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $updated ? self::STATUS_UPDATED : self::STATUS_NOTHING_TO_CHANGE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,19 @@ class Config
|
||||||
}
|
}
|
||||||
|
|
||||||
$json = file_get_contents($filename);
|
$json = file_get_contents($filename);
|
||||||
$this->data = json_decode($json, true);
|
$data = json_decode($json, true);
|
||||||
|
|
||||||
if ($this->data === null)
|
if (json_last_error() !== JSON_ERROR_NONE)
|
||||||
|
{
|
||||||
|
throw new \RuntimeException("Invalid JSON in config file: " . json_last_error_msg());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($data === null)
|
||||||
{
|
{
|
||||||
throw new RuntimeException("Invalid JSON in config file: $filename");
|
throw new RuntimeException("Invalid JSON in config file: $filename");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->data = $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get(string $key): mixed
|
public function get(string $key): mixed
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use App\Api\PorkbunAPI;
|
||||||
|
|
||||||
|
class PorkbunAPITest extends TestCase
|
||||||
|
{
|
||||||
|
private string $tmpConfig;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->tmpConfig = tempnam(sys_get_temp_dir(), 'cfg_');
|
||||||
|
file_put_contents($this->tmpConfig, json_encode([
|
||||||
|
"url" => "https://example.com/api/",
|
||||||
|
"apikey" => "TESTKEY",
|
||||||
|
"secretapikey" => "TESTSECRET"
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPingBuildsCorrectEndpointAndReturnsValue()
|
||||||
|
{
|
||||||
|
$api = $this->getMockBuilder(PorkbunAPI::class)
|
||||||
|
->setConstructorArgs([$this->tmpConfig])
|
||||||
|
->onlyMethods(['ping'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$api->expects($this->once())
|
||||||
|
->method('ping')
|
||||||
|
->willReturn('{"status":"SUCCESS"}');
|
||||||
|
|
||||||
|
$this->assertSame('{"status":"SUCCESS"}', $api->ping());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateReturnsExpectedValue()
|
||||||
|
{
|
||||||
|
$api = $this->getMockBuilder(PorkbunAPI::class)
|
||||||
|
->setConstructorArgs([$this->tmpConfig])
|
||||||
|
->onlyMethods(['create'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$api->expects($this->once())
|
||||||
|
->method('create')
|
||||||
|
->with("example.com", "www", "A", "1.2.3.4", 600)
|
||||||
|
->willReturn('{"status":"CREATED"}');
|
||||||
|
|
||||||
|
$result = $api->create("example.com", "www", "A", "1.2.3.4", 600);
|
||||||
|
|
||||||
|
$this->assertSame('{"status":"CREATED"}', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEditReturnsExpectedValue()
|
||||||
|
{
|
||||||
|
$api = $this->getMockBuilder(PorkbunAPI::class)
|
||||||
|
->setConstructorArgs([$this->tmpConfig])
|
||||||
|
->onlyMethods(['edit'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$api->expects($this->once())
|
||||||
|
->method('edit')
|
||||||
|
->with("example.com", "123", "5.6.7.8", "www")
|
||||||
|
->willReturn('{"status":"EDITED"}');
|
||||||
|
|
||||||
|
$result = $api->edit("example.com", "123", "5.6.7.8", "www");
|
||||||
|
|
||||||
|
$this->assertSame('{"status":"EDITED"}', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRetrieveReturnsExpectedValue()
|
||||||
|
{
|
||||||
|
$api = $this->getMockBuilder(PorkbunAPI::class)
|
||||||
|
->setConstructorArgs([$this->tmpConfig])
|
||||||
|
->onlyMethods(['retrieve'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$api->expects($this->once())
|
||||||
|
->method('retrieve')
|
||||||
|
->with("example.com", null)
|
||||||
|
->willReturn('{"records":[1,2,3]}');
|
||||||
|
|
||||||
|
$result = $api->retrieve("example.com");
|
||||||
|
|
||||||
|
$this->assertSame('{"records":[1,2,3]}', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRetrieveByNameTypeReturnsExpectedValue()
|
||||||
|
{
|
||||||
|
$api = $this->getMockBuilder(PorkbunAPI::class)
|
||||||
|
->setConstructorArgs([$this->tmpConfig])
|
||||||
|
->onlyMethods(['retrieveByNameType'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$api->expects($this->once())
|
||||||
|
->method('retrieveByNameType')
|
||||||
|
->with("example.com", "A", "www")
|
||||||
|
->willReturn('{"records":["A","B"]}');
|
||||||
|
|
||||||
|
$result = $api->retrieveByNameType("example.com", "A", "www");
|
||||||
|
|
||||||
|
$this->assertSame('{"records":["A","B"]}', $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use App\Api\PorkbunAPI;
|
||||||
|
use App\Command\UpdateDnsCommand;
|
||||||
|
use App\Config\Config;
|
||||||
|
use App\Util\Logger;
|
||||||
|
|
||||||
|
class UpdateDnsCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
private $config;
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->config = $this->createMock(Config::class);
|
||||||
|
$this->logger = $this->createMock(Logger::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scenario: DNS already matches public IP, so no update should occur:
|
||||||
|
public function testNoChangesNeeded()
|
||||||
|
{
|
||||||
|
$api = $this->createMock(PorkbunAPI::class);
|
||||||
|
|
||||||
|
// ping() must return yourIp or the command returns STATUS_ERROR immediately:
|
||||||
|
$api->method('ping')->willReturn('{"status":"SUCCESS","yourIp":"1.2.3.4"}');
|
||||||
|
|
||||||
|
// retrieve() must return JSON with a records array, otherwise json_decode() fails:
|
||||||
|
$api->method('retrieve')->willReturn(json_encode(
|
||||||
|
[
|
||||||
|
"records" => [
|
||||||
|
[
|
||||||
|
"name" => "test.example.com",
|
||||||
|
"type" => "A",
|
||||||
|
"content" => "1.2.3.4",
|
||||||
|
"id" => "123"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]));
|
||||||
|
|
||||||
|
$cmd = new UpdateDnsCommand($this->config, $this->logger);
|
||||||
|
$status = $cmd->run(["script.php", "example.com"], $api, "1.2.3.4");
|
||||||
|
|
||||||
|
$this->assertSame(UpdateDnsCommand::STATUS_NOTHING_TO_CHANGE, $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scenario: DNS is outdated, so edit() should be called and STATUS_UPDATED returned:
|
||||||
|
public function testDnsIsUpdated()
|
||||||
|
{
|
||||||
|
$api = $this->createMock(PorkbunAPI::class);
|
||||||
|
|
||||||
|
// ping() must return yourIp or the command returns STATUS_ERROR immediately:
|
||||||
|
$api->method('ping')->willReturn('{"status":"SUCCESS","yourIp":"1.2.3.4"}');
|
||||||
|
|
||||||
|
// retrieve() must return JSON with a records array, otherwise json_decode() fails:
|
||||||
|
$api->method('retrieve')->willReturn(json_encode(
|
||||||
|
[
|
||||||
|
"records" => [
|
||||||
|
[
|
||||||
|
"name" => "test.example.com",
|
||||||
|
"type" => "A",
|
||||||
|
"content" => "5.6.7.8",
|
||||||
|
"id" => "123"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]));
|
||||||
|
|
||||||
|
// edit() must return a SUCCESS status or the command returns STATUS_ERROR:
|
||||||
|
$api->method('edit')->willReturn(json_encode(["status" => "SUCCESS"]));
|
||||||
|
|
||||||
|
// When DNS is updated, the command must record the change in the log exactly once:
|
||||||
|
$this->logger->expects($this->once())->method('log')->with($this->stringContains("Updated DNS"));
|
||||||
|
|
||||||
|
$cmd = new UpdateDnsCommand($this->config, $this->logger);
|
||||||
|
$status = $cmd->run(["script.php", "example.com"], $api, "1.2.3.4");
|
||||||
|
|
||||||
|
$this->assertSame(UpdateDnsCommand::STATUS_UPDATED, $status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use App\Config\Config;
|
||||||
|
|
||||||
|
class ConfigTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testLoadsValidJson()
|
||||||
|
{
|
||||||
|
$tmp = tempnam(sys_get_temp_dir(), 'cfg_');
|
||||||
|
file_put_contents($tmp, json_encode(["foo" => "bar"]));
|
||||||
|
|
||||||
|
$config = new Config($tmp);
|
||||||
|
|
||||||
|
$this->assertSame("bar", $config->get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsOnMissingFile()
|
||||||
|
{
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
new Config("/nonexistent/file.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsOnInvalidJson()
|
||||||
|
{
|
||||||
|
$tmp = tempnam(sys_get_temp_dir(), 'cfg_');
|
||||||
|
file_put_contents($tmp, "{invalid json");
|
||||||
|
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
new Config($tmp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use App\Util\Logger;
|
||||||
|
|
||||||
|
class LoggerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testLogWritesMessage()
|
||||||
|
{
|
||||||
|
$tmp = tempnam(sys_get_temp_dir(), 'log_');
|
||||||
|
$logger = new Logger($tmp);
|
||||||
|
|
||||||
|
$logger->log("Hello world");
|
||||||
|
|
||||||
|
$contents = file_get_contents($tmp);
|
||||||
|
|
||||||
|
$this->assertStringContainsString("Hello world", $contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue