Compare commits
No commits in common. "0feb341664cf2fb0cf0b564204dfad8911aa2160" and "ab2dca6c27ab05b78d58c653b6b3e53aad5a3b05" have entirely different histories.
0feb341664
...
ab2dca6c27
|
|
@ -1,9 +0,0 @@
|
||||||
# Config files:
|
|
||||||
/config/*.json
|
|
||||||
!/config/pb-dydns.example.json
|
|
||||||
|
|
||||||
# Composer files:
|
|
||||||
/vendor/
|
|
||||||
|
|
||||||
# PHPUnit files:
|
|
||||||
.phpunit.result.cache
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Api;
|
//
|
||||||
|
// Outputs a message to CLI, if this script is run from CLI.
|
||||||
|
//
|
||||||
|
|
||||||
use stdClass;
|
function echo_to_cli(string $message)
|
||||||
use App\Util\Console;
|
{
|
||||||
|
if (php_sapi_name() === 'cli') echo $message;
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Wrapper class to communicate with Porkbun API.
|
// Wrapper class to communicate with Porkbun API.
|
||||||
|
|
@ -11,11 +15,15 @@ use App\Util\Console;
|
||||||
|
|
||||||
class PorkbunAPI
|
class PorkbunAPI
|
||||||
{
|
{
|
||||||
private stdClass $config;
|
|
||||||
|
|
||||||
function __construct(string $config_filename)
|
function __construct(string $config_filename)
|
||||||
{
|
{
|
||||||
$this->config = json_decode(file_get_contents($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:
|
// Create the correct endpoint based on the URL:
|
||||||
$endpoint = $this->config->url . "ping";
|
$endpoint = $this->config->url . "ping";
|
||||||
|
|
||||||
$ch = curl_init();
|
|
||||||
|
|
||||||
// Set some cURL options:
|
// Set some cURL options:
|
||||||
curl_setopt($ch, CURLOPT_URL, $endpoint);
|
curl_setopt($this->ch, CURLOPT_URL, $endpoint);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
|
||||||
curl_setopt($ch, CURLOPT_POST, 1);
|
curl_setopt($this->ch, CURLOPT_POST, 1);
|
||||||
|
|
||||||
// Tell the server that the request body contains JSON data:
|
// Tell the server that the request body contains JSON data:
|
||||||
$headers = array();
|
$headers = array();
|
||||||
$headers[] = "Content-Type: application/json";
|
$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:
|
// Porkbun API wants to be passed data in JSON format:
|
||||||
$json = array
|
$json = array
|
||||||
|
|
@ -48,13 +54,13 @@ class PorkbunAPI
|
||||||
"secretapikey" => $this->config->secretapikey
|
"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:
|
// Execute cURL and return the result:
|
||||||
$result = curl_exec($ch);
|
$result = curl_exec($this->ch);
|
||||||
if (curl_errno($ch))
|
if (curl_errno($this->ch))
|
||||||
{
|
{
|
||||||
Console::echo("Error: " . curl_error($ch));
|
echo_to_cli("Error: " . curl_error($this->ch));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,17 +78,15 @@ class PorkbunAPI
|
||||||
// Create the correct endpoint based on the URL:
|
// Create the correct endpoint based on the URL:
|
||||||
$endpoint = $this->config->url . "dns/create/" . $domain;
|
$endpoint = $this->config->url . "dns/create/" . $domain;
|
||||||
|
|
||||||
$ch = curl_init();
|
|
||||||
|
|
||||||
// Set some cURL options:
|
// Set some cURL options:
|
||||||
curl_setopt($ch, CURLOPT_URL, $endpoint);
|
curl_setopt($this->ch, CURLOPT_URL, $endpoint);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
|
||||||
curl_setopt($ch, CURLOPT_POST, 1);
|
curl_setopt($this->ch, CURLOPT_POST, 1);
|
||||||
|
|
||||||
// Tell the server that the request body contains JSON data:
|
// Tell the server that the request body contains JSON data:
|
||||||
$headers = array();
|
$headers = array();
|
||||||
$headers[] = "Content-Type: application/json";
|
$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:
|
// Porkbun API wants to be passed data in JSON format:
|
||||||
$json = array
|
$json = array
|
||||||
|
|
@ -95,13 +99,13 @@ class PorkbunAPI
|
||||||
"ttl" => "$ttl"
|
"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:
|
// Execute cURL and return the result:
|
||||||
$result = curl_exec($ch);
|
$result = curl_exec($this->ch);
|
||||||
if (curl_errno($ch))
|
if (curl_errno($this->ch))
|
||||||
{
|
{
|
||||||
Console::echo("Error: " . curl_error($ch));
|
echo_to_cli("Error: " . curl_error($this->ch));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,17 +124,15 @@ class PorkbunAPI
|
||||||
$endpoint = $this->config->url . "dns/edit/" . $domain;
|
$endpoint = $this->config->url . "dns/edit/" . $domain;
|
||||||
$endpoint .= "/" . $id;
|
$endpoint .= "/" . $id;
|
||||||
|
|
||||||
$ch = curl_init();
|
|
||||||
|
|
||||||
// Set some cURL options:
|
// Set some cURL options:
|
||||||
curl_setopt($ch, CURLOPT_URL, $endpoint);
|
curl_setopt($this->ch, CURLOPT_URL, $endpoint);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
|
||||||
curl_setopt($ch, CURLOPT_POST, 1);
|
curl_setopt($this->ch, CURLOPT_POST, 1);
|
||||||
|
|
||||||
// Tell the server that the request body contains JSON data:
|
// Tell the server that the request body contains JSON data:
|
||||||
$headers = array();
|
$headers = array();
|
||||||
$headers[] = "Content-Type: application/json";
|
$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:
|
// Porkbun API wants the API key and secret in a JSON structure:
|
||||||
$json = array
|
$json = array
|
||||||
|
|
@ -142,13 +144,13 @@ class PorkbunAPI
|
||||||
"content" => "$content"
|
"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:
|
// Execute cURL and return the result:
|
||||||
$result = curl_exec($ch);
|
$result = curl_exec($this->ch);
|
||||||
if (curl_errno($ch))
|
if (curl_errno($this->ch))
|
||||||
{
|
{
|
||||||
Console::echo("Error: " . curl_error($ch));
|
echo_to_cli("Error: " . curl_error($this->ch));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,23 +163,21 @@ class PorkbunAPI
|
||||||
// https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain%20or%20ID
|
// 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:
|
// Create the correct endpoint based on the URL:
|
||||||
$endpoint = $this->config->url . "dns/retrieve/" . $domain;
|
$endpoint = $this->config->url . "dns/retrieve/" . $domain;
|
||||||
if (!is_null($id)) $endpoint .= "/" . $id;
|
if (!is_null($id)) $endpoint .= "/" . $id;
|
||||||
|
|
||||||
$ch = curl_init();
|
|
||||||
|
|
||||||
// Set some cURL options:
|
// Set some cURL options:
|
||||||
curl_setopt($ch, CURLOPT_URL, $endpoint);
|
curl_setopt($this->ch, CURLOPT_URL, $endpoint);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
|
||||||
curl_setopt($ch, CURLOPT_POST, 1);
|
curl_setopt($this->ch, CURLOPT_POST, 1);
|
||||||
|
|
||||||
// Tell the server that the request body contains JSON data:
|
// Tell the server that the request body contains JSON data:
|
||||||
$headers = array();
|
$headers = array();
|
||||||
$headers[] = "Content-Type: application/json";
|
$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:
|
// Porkbun API wants the API key and secret in a JSON structure:
|
||||||
$json = array
|
$json = array
|
||||||
|
|
@ -186,13 +186,13 @@ class PorkbunAPI
|
||||||
"secretapikey" => $this->config->secretapikey
|
"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:
|
// Execute cURL and return the result:
|
||||||
$result = curl_exec($ch);
|
$result = curl_exec($this->ch);
|
||||||
if (curl_errno($ch))
|
if (curl_errno($this->ch))
|
||||||
{
|
{
|
||||||
Console::echo("Error: " . curl_error($ch));
|
echo_to_cli("Error: " . curl_error($this->ch));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,23 +205,21 @@ class PorkbunAPI
|
||||||
// https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain,%20Subdomain%20and%20Type
|
// 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:
|
// Create the correct endpoint based on the URL:
|
||||||
$endpoint = $this->config->url . "dns/retrieveByNameType/" . $domain . "/" . $type;
|
$endpoint = $this->config->url . "dns/retrieveByNameType/" . $domain . "/" . $type;
|
||||||
if (!is_null($subdomain)) $endpoint .= "/" . $subdomain;
|
if (!is_null($subdomain)) $endpoint .= "/" . $subdomain;
|
||||||
|
|
||||||
$ch = curl_init();
|
|
||||||
|
|
||||||
// Set some cURL options:
|
// Set some cURL options:
|
||||||
curl_setopt($ch, CURLOPT_URL, $endpoint);
|
curl_setopt($this->ch, CURLOPT_URL, $endpoint);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
|
||||||
curl_setopt($ch, CURLOPT_POST, 1);
|
curl_setopt($this->ch, CURLOPT_POST, 1);
|
||||||
|
|
||||||
// Tell the server that the request body contains JSON data:
|
// Tell the server that the request body contains JSON data:
|
||||||
$headers = array();
|
$headers = array();
|
||||||
$headers[] = "Content-Type: application/json";
|
$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:
|
// Porkbun API wants the API key and secret in a JSON structure:
|
||||||
$json = array
|
$json = array
|
||||||
|
|
@ -230,18 +228,22 @@ class PorkbunAPI
|
||||||
"secretapikey" => $this->config->secretapikey
|
"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:
|
// Execute cURL and return the result:
|
||||||
$result = curl_exec($ch);
|
$result = curl_exec($this->ch);
|
||||||
if (curl_errno($ch))
|
if (curl_errno($this->ch))
|
||||||
{
|
{
|
||||||
Console::echo("Error: " . curl_error($ch));
|
echo_to_cli("Error: " . curl_error($this->ch));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
?>
|
?>
|
||||||
83
README.md
83
README.md
|
|
@ -2,82 +2,43 @@
|
||||||
|
|
||||||
Script in PHP to update DNS type A records on porkbun.com using their API.
|
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
|
## Prerequisites
|
||||||
|
|
||||||
- At least one domain in porkbun.com with DNS type A records already pointing to your dynamic IP address.
|
- 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).
|
- Access to Porkbun API.
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
## How to use it
|
## 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
|
### as a cron job
|
||||||
|
|
||||||
Edit your crontab:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
crontab -e
|
|
||||||
```
|
|
||||||
|
|
||||||
Add a line like:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
*/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
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -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));
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"url": "https://api.porkbun.com/api/json/v3/",
|
|
||||||
"apikey": "YOUR_API_KEY_HERE",
|
|
||||||
"secretapikey": "YOUR_SECRET_KEY_HERE"
|
|
||||||
}
|
|
||||||
35
deploy.sh
35
deploy.sh
|
|
@ -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."
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Util;
|
|
||||||
|
|
||||||
class Console
|
|
||||||
{
|
|
||||||
public static function echo(string $message): void
|
|
||||||
{
|
|
||||||
fwrite(STDOUT, $message . PHP_EOL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue