Compare commits

..

No commits in common. "0feb341664cf2fb0cf0b564204dfad8911aa2160" and "ab2dca6c27ab05b78d58c653b6b3e53aad5a3b05" have entirely different histories.

18 changed files with 225 additions and 2323 deletions

9
.gitignore vendored
View File

@ -1,9 +0,0 @@
# Config files:
/config/*.json
!/config/pb-dydns.example.json
# Composer files:
/vendor/
# PHPUnit files:
.phpunit.result.cache

View File

@ -1,9 +1,13 @@
<?php
namespace App\Api;
//
// Outputs a message to CLI, if this script is run from CLI.
//
use stdClass;
use App\Util\Console;
function echo_to_cli(string $message)
{
if (php_sapi_name() === 'cli') echo $message;
}
//
// Wrapper class to communicate with Porkbun API.
@ -11,11 +15,15 @@ use App\Util\Console;
class PorkbunAPI
{
private stdClass $config;
function __construct(string $config_filename)
{
$this->config = json_decode(file_get_contents($config_filename));
$this->ch = curl_init();
}
function __destruct()
{
curl_close($this->ch);
}
//
@ -29,17 +37,15 @@ class PorkbunAPI
// Create the correct endpoint based on the URL:
$endpoint = $this->config->url . "ping";
$ch = curl_init();
// Set some cURL options:
curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($this->ch, CURLOPT_URL, $endpoint);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($this->ch, CURLOPT_POST, 1);
// Tell the server that the request body contains JSON data:
$headers = array();
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
// Porkbun API wants to be passed data in JSON format:
$json = array
@ -48,13 +54,13 @@ class PorkbunAPI
"secretapikey" => $this->config->secretapikey
);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json));
// Execute cURL and return the result:
$result = curl_exec($ch);
if (curl_errno($ch))
$result = curl_exec($this->ch);
if (curl_errno($this->ch))
{
Console::echo("Error: " . curl_error($ch));
echo_to_cli("Error: " . curl_error($this->ch));
exit;
}
@ -72,17 +78,15 @@ class PorkbunAPI
// Create the correct endpoint based on the URL:
$endpoint = $this->config->url . "dns/create/" . $domain;
$ch = curl_init();
// Set some cURL options:
curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($this->ch, CURLOPT_URL, $endpoint);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($this->ch, CURLOPT_POST, 1);
// Tell the server that the request body contains JSON data:
$headers = array();
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
// Porkbun API wants to be passed data in JSON format:
$json = array
@ -95,13 +99,13 @@ class PorkbunAPI
"ttl" => "$ttl"
);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json));
// Execute cURL and return the result:
$result = curl_exec($ch);
if (curl_errno($ch))
$result = curl_exec($this->ch);
if (curl_errno($this->ch))
{
Console::echo("Error: " . curl_error($ch));
echo_to_cli("Error: " . curl_error($this->ch));
exit;
}
@ -120,17 +124,15 @@ class PorkbunAPI
$endpoint = $this->config->url . "dns/edit/" . $domain;
$endpoint .= "/" . $id;
$ch = curl_init();
// Set some cURL options:
curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($this->ch, CURLOPT_URL, $endpoint);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($this->ch, CURLOPT_POST, 1);
// Tell the server that the request body contains JSON data:
$headers = array();
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
// Porkbun API wants the API key and secret in a JSON structure:
$json = array
@ -142,13 +144,13 @@ class PorkbunAPI
"content" => "$content"
);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json));
// Execute cURL and return the result:
$result = curl_exec($ch);
if (curl_errno($ch))
$result = curl_exec($this->ch);
if (curl_errno($this->ch))
{
Console::echo("Error: " . curl_error($ch));
echo_to_cli("Error: " . curl_error($this->ch));
exit;
}
@ -161,23 +163,21 @@ class PorkbunAPI
// https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain%20or%20ID
//
function retrieve(string $domain, ?string $id = null)
function retrieve(string $domain, string $id = null)
{
// Create the correct endpoint based on the URL:
$endpoint = $this->config->url . "dns/retrieve/" . $domain;
if (!is_null($id)) $endpoint .= "/" . $id;
$ch = curl_init();
// Set some cURL options:
curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($this->ch, CURLOPT_URL, $endpoint);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($this->ch, CURLOPT_POST, 1);
// Tell the server that the request body contains JSON data:
$headers = array();
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
// Porkbun API wants the API key and secret in a JSON structure:
$json = array
@ -186,13 +186,13 @@ class PorkbunAPI
"secretapikey" => $this->config->secretapikey
);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json));
// Execute cURL and return the result:
$result = curl_exec($ch);
if (curl_errno($ch))
$result = curl_exec($this->ch);
if (curl_errno($this->ch))
{
Console::echo("Error: " . curl_error($ch));
echo_to_cli("Error: " . curl_error($this->ch));
exit;
}
@ -205,23 +205,21 @@ class PorkbunAPI
// https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain,%20Subdomain%20and%20Type
//
function retrieveByNameType(string $domain, string $type, ?string $subdomain = null)
function retrieveByNameType(string $domain, string $type, string $subdomain = null)
{
// Create the correct endpoint based on the URL:
$endpoint = $this->config->url . "dns/retrieveByNameType/" . $domain . "/" . $type;
if (!is_null($subdomain)) $endpoint .= "/" . $subdomain;
$ch = curl_init();
// Set some cURL options:
curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($this->ch, CURLOPT_URL, $endpoint);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($this->ch, CURLOPT_POST, 1);
// Tell the server that the request body contains JSON data:
$headers = array();
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
// Porkbun API wants the API key and secret in a JSON structure:
$json = array
@ -230,18 +228,22 @@ class PorkbunAPI
"secretapikey" => $this->config->secretapikey
);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json));
// Execute cURL and return the result:
$result = curl_exec($ch);
if (curl_errno($ch))
$result = curl_exec($this->ch);
if (curl_errno($this->ch))
{
Console::echo("Error: " . curl_error($ch));
echo_to_cli("Error: " . curl_error($this->ch));
exit;
}
return $result;
}
// These properties don't need to be declared as they would be dynamically created when assigned a value:
private stdClass $config;
private CurlHandle $ch;
};
?>

View File

@ -2,82 +2,43 @@
Script in PHP to update DNS type A records on porkbun.com using their API.
This can easily be adapted to other registrars or scripting languages.
This can easily be modified to a different programming or scripting language and domain registrar.
## Prerequisites
- At least one domain in porkbun.com with DNS type A records already pointing to your dynamic IP address.
- Access to the Porkbun API (API key + secret key).
## Installation & Deployment
### 1. Clone the repository
```bash
git clone https://gitea.ramoncaballero.dev/mon/pb-dydns.git
cd pb-dydns
```
### 2. Deploy the script
Run the provided deployment script:
```bash
./deploy.sh
```
This creates a directory:
```
$HOME/pb-dydns-live
```
Inside it you will find:
- pb-dydns.php (the script you run)
- PorkbunAPI.php
- UpdateDnsCommand.php
- Config.php
- Logger.php
- pb-dydns.json (your configuration file)
- pb-dydns.log (log file)
### 3. Configure it
Edit the configuration file `$HOME/pb-dydns-live/pb-dydns.json`
Fill in:
- the Porkbun API endpoint (usually https://api.porkbun.com/api/json/v3/)
- your Porkbun API key
- your secret key
- Access to Porkbun API.
## How to use it
### From the command line
### from command line
```
$ php /path/to/pb-dydns-live/pb-dydns.php yourdomain.com
$ php /path/to/pb-dydns.php domain_name
```
### As a cron job
Edit your crontab:
```bash
crontab -e
```
Add a line like:
### as a cron job
```
*/10 * * * * php /path/to/pb-dydns-live/pb-dydns.php yourdomain.com > /dev/null
$ crontab -e
```
This runs the updater every 10 minutes.
Modify this line to fit your needs, and add it as many times as domains you want to automatically update:
## Logs
```
*/10 * * * * php /path/to/pb-dydns.php domain_name > /dev/null
```
DNS updates are written to `/path/to/pb-dydns-live/pb-dydns.log`
That will run the script every 10 minutes.
Runs where nothing changes do not produce log entries.
Then restart cron (I'm not sure if this is necessary):
```
$ sudo systemctl restart cron
```
Entries to `pb-dydns.log` will be added, to view it you can:
```
$ cat /path/to/pb-dydns.log
```

View File

@ -1,14 +0,0 @@
#!/usr/bin/env php
<?php
require __DIR__ . '/../vendor/autoload.php';
use App\Config\Config;
use App\Util\Logger;
use App\Command\UpdateDnsCommand;
$config = new Config(__DIR__ . "/../config/pb-dydns.json");
$logger = new Logger(__DIR__ . "/../logs/pb-dydns.log");
$cmd = new UpdateDnsCommand($config, $logger);
exit($cmd->run($argv));

View File

@ -1,16 +0,0 @@
{
"name": "ramoncaballero.dev/pb-dydns",
"description": "Dynamic DNS updater for Porkbun",
"type": "project",
"autoload":
{
"psr-4":
{
"App\\": "src/"
}
},
"require-dev":
{
"phpunit/phpunit": "^12.5"
}
}

1690
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +0,0 @@
{
"url": "https://api.porkbun.com/api/json/v3/",
"apikey": "YOUR_API_KEY_HERE",
"secretapikey": "YOUR_SECRET_KEY_HERE"
}

View File

@ -1,35 +0,0 @@
#!/bin/bash
set -e
DEPLOY_DIR="$HOME/pb-dydns-live"
echo "Deploying to $DEPLOY_DIR"
mkdir -p "$DEPLOY_DIR"
# Copy main script:
cp bin/pb-dydns.php "$DEPLOY_DIR/"
# Copy all PHP source files into a flat structure:
cp src/Api/PorkbunAPI.php "$DEPLOY_DIR/"
cp src/Command/UpdateDnsCommand.php "$DEPLOY_DIR/"
cp src/Config/Config.php "$DEPLOY_DIR/"
cp src/Util/Logger.php "$DEPLOY_DIR/"
# Copy example config only if user doesn't already have a real one:
if [ ! -f "$DEPLOY_DIR/pb-dydns.json" ]; then
cp config/pb-dydns.example.json "$DEPLOY_DIR/pb-dydns.json"
echo "Created default configuration file: pb-dydns.json"
else
echo "Existing pb-dydns.json preserved"
fi
# Ensure log file exists:
touch "$DEPLOY_DIR/pb-dydns.log"
# Make script executable:
chmod +x "$DEPLOY_DIR/pb-dydns.php"
echo "Deployment complete."
echo "Remember to edit $DEPLOY_DIR/pb-dydns.json with your Porkbun API keys."

144
pb-dydns.php Executable file
View File

@ -0,0 +1,144 @@
<?php
//
// This script updates the DNS type A on porkbun.com so it is always pointing to your public IP address.
//
// Dependencies:
//
// - PorkbunAPI.php
// - pb-dydns.json (it will be created automatically the 1st time)
//
// How to use it:
//
// - from command line:
//
// $ php /path/to/pb-dydns.php domain_name
//
// It will retrieve all DNS records for the specificied domain_name and update the A type ones
// with your public IP address.
//
//- as a cron job, to add it to:
//
// $ crontab -e
//
// Append this line as many times as domains you want to automatically update:
//
// */10 * * * * php /path/to/pb-dydns.php domain_name > /dev/null
//
// And restart cron (I'm not sure if this is necessary):
//
// $ sudo systemctl restart cron
//
// Entries to a log file called "pb-dydns.log" will be added, to view it you can:
//
// $ cat /path/to/pb-dydns.log
//
require "PorkbunAPI.php";
// Make sure that pb-dydns.json exists:
$config_filename = __DIR__ . "/pb-dydns.json";
if (!file_exists($config_filename))
{
echo "The config file $config_filename does not exist." . PHP_EOL;
$config = json_encode(array
(
"url" => "https://porkbun.com/api/json/v3/",
"apikey" => "",
"secretapikey" => ""
), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
file_put_contents($config_filename, $config);
echo "It has been created but you must edit it with your API key and secret." . PHP_EOL;
echo "After that you can try again." . PHP_EOL;
exit(1);
}
// Domain name must be provided as argument.
// There should be 2 arguments as $argc includes the script name itself:
if ($argc != 2)
{
echo_to_cli("This script requires a domain name as argument:" . PHP_EOL);
echo_to_cli("php $argv[0] domain_name" . PHP_EOL);
exit(2);
}
// Take the arguments in individual variables:
$myDomain = $argv[1];
// Create an instance of PorkbunAPI:
$pbapi = new PorkbunAPI($config_filename);
// Test connection to Porkbun API in order to get the public IP:
$raw = $pbapi->ping();
$result = json_decode($raw);
if ($result === null)
{
echo_to_cli("Invalid JSON returned by Porkbun API:\n$raw\n");
exit(3);
}
if (!isset($result->status))
{
echo_to_cli("Porkbun API response missing 'status' field:\n$raw\n");
exit(3);
}
if ($result->status !== "SUCCESS")
{
echo_to_cli("Porkbun API returned an error:\n$raw\n");
exit(3);
}
if (!isset($result->yourIp))
{
echo_to_cli("Porkbun API did not return your public IP.\n");
exit(3);
}
$myIp = $result->yourIp;
echo_to_cli("Your public IP address is $myIp" . PHP_EOL);
// Retrieve all DNS records associated with user's domain:
$records = $pbapi->retrieve($myDomain);
// Discard those records that are not type "A":
$data = json_decode($records);
$filteredRecords = array_filter($data->records, function ($record)
{
return $record->type == "A";
});
$log_filename = __DIR__ . "/pb-dydns.log";
// Update the records that passed the filter with the public IP:
foreach ($filteredRecords as $record)
{
echo_to_cli("Porkbun's DNS for $record->name is pointing to $record->content... ");
if ($record->content != $myIp)
{
echo_to_cli("Let's change that... ");
$name = rtrim(strstr($record->name, $myDomain, true), ".");
$result = json_decode($pbapi->edit($myDomain, $record->id, $myIp, $name));
if ($result->status == "SUCCESS") echo_to_cli("Done!" . PHP_EOL);
$now = date('Y-m-d H:i:s');
$message = "$now: Updated DNS on $record->name from $record->content to $myIp" . PHP_EOL;
file_put_contents($log_filename, $message, FILE_APPEND);
}
else
{
echo_to_cli("Nothing needs to be changed!" . PHP_EOL);
}
}
?>

View File

@ -1,8 +0,0 @@
<?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>

View File

@ -1,129 +0,0 @@
<?php
namespace App\Command;
use App\Api\PorkbunAPI;
use App\Config\Config;
use App\Util\Logger;
use App\Util\Console;
class UpdateDnsCommand
{
public const STATUS_NOTHING_TO_CHANGE = 0;
public const STATUS_UPDATED = 1;
public const STATUS_ERROR = 2;
private Config $config;
private Logger $logger;
public function __construct(Config $config, Logger $logger)
{
$this->config = $config;
$this->logger = $logger;
}
public function run(array $argv, ?PorkbunAPI $api = null, ?string $myIp = null): int
{
if (count($argv) < 2)
{
echo "Usage: pb-dydns.php <domain>\n";
return self::STATUS_ERROR;
}
$domain = $argv[1];
// Allow API injection for tests:
$api = $api ?? new PorkbunAPI($this->config->get('config_filename'));
// 1. Test connection to Porkbun API in order to get the public IP:
$raw = $api->ping();
$result = json_decode($raw);
if ($result === null)
{
return $this->fail("Invalid JSON returned by Porkbun API: " . $raw);
}
if (!isset($result->status))
{
return $this->fail("Porkbun API response missing 'status' field: " . $raw);
}
if ($result->status !== "SUCCESS")
{
return $this->fail("Porkbun API returned an error: " . $raw);
}
if (!isset($result->yourIp))
{
return $this->fail("Porkbun API did not return your public IP.");
}
$myIp = $myIp ?? $result->yourIp;
Console::echo("Your public IP address is $myIp" . PHP_EOL);
// 2. Retrieve all DNS records associated with user's domain:
$records = $api->retrieve($domain);
$data = json_decode($records);
if ($data === null || !isset($data->records))
{
return $this->fail("Invalid DNS records returned by Porkbun API: " . $records);
}
// Discard those records that are not type "A":
$filteredRecords = array_filter($data->records, function ($record)
{
return isset($record->type) && $record->type == "A";
});
// 3. Update the records that passed the filter with the public IP:
$updated = false;
foreach ($filteredRecords as $record)
{
if (!isset($record->id, $record->name, $record->content, $record->type))
{
return $this->fail("Porkbun API returned a malformed DNS record.");
}
Console::echo("Porkbun's DNS for $record->name is pointing to $record->content... ");
if ($record->content != $myIp)
{
Console::echo("Let's change that... ");
$name = rtrim(strstr($record->name, $domain, true), ".");
$result = json_decode($api->edit($domain, $record->id, $myIp, $name));
if ($result && isset($result->status) && $result->status === "SUCCESS")
{
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;
}
}
else
{
Console::echo("Nothing needs to be changed!" . PHP_EOL);
}
Console::echo("");
}
return $updated ? self::STATUS_UPDATED : self::STATUS_NOTHING_TO_CHANGE;
}
private function fail(string $message): int
{
Console::echo($message);
$this->logger->log("ERROR: " . $message);
return self::STATUS_ERROR;
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Config;
use RuntimeException;
class Config
{
private array $data;
public function __construct(string $filename)
{
if (!file_exists($filename))
{
throw new RuntimeException("Config file not found: $filename");
}
$json = file_get_contents($filename);
$data = json_decode($json, true);
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");
}
$this->data = $data;
}
public function get(string $key): mixed
{
return $this->data[$key] ?? null;
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace App\Util;
class Console
{
public static function echo(string $message): void
{
fwrite(STDOUT, $message . PHP_EOL);
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Util;
class Logger
{
private string $filename;
public function __construct(string $filename)
{
$this->filename = $filename;
}
public function log(string $message): void
{
$timestamp = date("Y-m-d H:i:s");
file_put_contents($this->filename, $timestamp . ' ' . $message . PHP_EOL, FILE_APPEND);
}
}

View File

@ -1,101 +0,0 @@
<?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);
}
}

View File

@ -1,79 +0,0 @@
<?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);
}
}

View File

@ -1,32 +0,0 @@
<?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);
}
}

View File

@ -1,19 +0,0 @@
<?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);
}
}