Restructure rules, rewrite config, starts tests
Rules now distinguish between user errors and bugs, give more specific errors, configuration now uses a completely different interface, and start with the new setup for tests.
This commit is contained in:
parent
2ef6b105ce
commit
732298bf09
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "fwdekker/death-notifier",
|
||||
"description": "Get notified when a famous person dies.",
|
||||
"version": "0.18.2", "_comment_version": "Also update version in `package.json`!",
|
||||
"version": "0.19.0", "_comment_version": "Also update version in `package.json`!",
|
||||
"type": "project",
|
||||
"license": "MIT",
|
||||
"homepage": "https://git.fwdekker.com/tools/death-notifier",
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "death-notifier",
|
||||
"version": "0.18.2", "_comment_version": "Also update version in `composer.json`!",
|
||||
"version": "0.19.0", "_comment_version": "Also update version in `composer.json`!",
|
||||
"description": "Get notified when a famous person dies.",
|
||||
"author": "Florine W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
|
|
@ -45,7 +45,7 @@ try {
|
|||
session_start();
|
||||
$_SESSION["token"] = $_SESSION["token"] ?? Util::generate_csrf_token();
|
||||
|
||||
$db = new Database(Config::get()["database"]["filename"]);
|
||||
$db = new Database(Config::get("database.filename"));
|
||||
|
||||
$wikipedia = new Wikipedia();
|
||||
$email_queue = new EmailQueue($db);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
[admin]
|
||||
# bcrypt hash of password to use the CLI of `api.php`. If set to its default value, or if empty, the CLI is disabled.
|
||||
cli_secret = REPLACE THIS WITH A SECRET VALUE
|
||||
cli_password = REPLACE THIS WITH A SECRET VALUE
|
||||
|
||||
[database]
|
||||
# Relative path to SQLite database.
|
||||
|
|
|
@ -9,9 +9,8 @@ use InvalidArgumentException;
|
|||
/**
|
||||
* The application's configuration, read lazily from configuration files.
|
||||
*
|
||||
* Contains global state, but that's fine since it's read-only.
|
||||
* Contains global state, but that's fine since it's read-only (except for some internal functions for testing).
|
||||
*/
|
||||
// TODO: Override dynamically from tests
|
||||
class Config
|
||||
{
|
||||
/**
|
||||
|
@ -21,16 +20,88 @@ class Config
|
|||
|
||||
|
||||
/**
|
||||
* Returns the application's configuration.
|
||||
* Returns the property at {@see $index}.
|
||||
*
|
||||
* @return array<string, mixed> the application's configuration
|
||||
* To get the property `name` from section `section`, set `$index` to `"name.section"`. To return the entire section
|
||||
* `section` as an array, set `$index` to `"section"`.
|
||||
*
|
||||
* @param string|null $index the index of the property to return, using `.` as a nesting delimiter; or `null` to
|
||||
* return the entire configuration
|
||||
* @return mixed the property at {@see $index}
|
||||
*/
|
||||
public static function get(): array
|
||||
public static function get(?string $index = null): mixed
|
||||
{
|
||||
if (self::$config === null)
|
||||
self::$config = self::read_config();
|
||||
self::$config = self::read_config_from_file();
|
||||
|
||||
return self::$config;
|
||||
if ($index === null)
|
||||
return self::$config;
|
||||
|
||||
$output = self::$config;
|
||||
foreach (explode(".", $index) as $key)
|
||||
$output = $output[$key];
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if and only if the property at {@see $index} is not `null`.
|
||||
*
|
||||
* @param string $index the index of the property to check, using `.` as a nesting delimiter
|
||||
* @return bool `true` if and only if the property at {@see $index} is not `null`
|
||||
*/
|
||||
public static function has(string $index): bool
|
||||
{
|
||||
$output = self::get();
|
||||
|
||||
foreach (explode(".", $index) as $key) {
|
||||
if (!isset($output[$key]))
|
||||
return false;
|
||||
|
||||
$output = $output[$key];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the property at {@see $index} to {@see $value}.
|
||||
*
|
||||
* This function is intended for testability. It should never be used in production code.
|
||||
*
|
||||
* @param string $index the index to set to {@see $value}, using `.` as a nesting delimiter
|
||||
* @param mixed $value the value to write at {@see $index}
|
||||
* @return void
|
||||
* @see Config::_reset()
|
||||
*/
|
||||
public static function _set(string $index, mixed $value): void
|
||||
{
|
||||
self::get();
|
||||
|
||||
$keys = explode(".", $index);
|
||||
$last_key = array_pop($keys);
|
||||
|
||||
$output = &self::$config;
|
||||
foreach ($keys as $key) {
|
||||
if (!isset($output[$key]))
|
||||
$output[$key] = [];
|
||||
|
||||
$output = &$output[$key];
|
||||
}
|
||||
$output[$last_key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all manual modifications to the application's configuration.
|
||||
*
|
||||
* This function is intended for testability. It should never be used in production code.
|
||||
*
|
||||
* @return void
|
||||
* @see Config::_set()
|
||||
*/
|
||||
public static function _reset(): void
|
||||
{
|
||||
self::$config = null;
|
||||
}
|
||||
|
||||
|
||||
|
@ -39,7 +110,7 @@ class Config
|
|||
*
|
||||
* @return array<string, mixed> the application's configuration
|
||||
*/
|
||||
private static function read_config(): array
|
||||
private static function read_config_from_file(): array
|
||||
{
|
||||
$config = parse_ini_file("config.default.ini.php", process_sections: true, scanner_mode: INI_SCANNER_TYPED);
|
||||
if ($config === false) throw new InvalidArgumentException("Invalid `config.default.ini.php` file.");
|
||||
|
@ -48,7 +119,7 @@ class Config
|
|||
$config_custom = parse_ini_file("config.ini.php", process_sections: true, scanner_mode: INI_SCANNER_TYPED);
|
||||
if ($config_custom === false) throw new InvalidArgumentException("Invalid `config.ini.php` file.");
|
||||
|
||||
$config = array_replace_recursive($config, $config_custom);
|
||||
$config = Util::array_merge_recursive_distinct($config, $config_custom);
|
||||
|
||||
// Check file permissions
|
||||
if (!$config["security"]["allow_config_insecure_permissions"]) {
|
||||
|
|
|
@ -30,7 +30,7 @@ class LoggerUtil
|
|||
public static function with_name(string $name = "main"): Logger
|
||||
{
|
||||
if (self::$main_logger === null) {
|
||||
$config = Config::get()["logger"];
|
||||
$config = Config::get("logger");
|
||||
|
||||
self::$main_logger = new Logger("main");
|
||||
self::$main_logger->pushHandler(new StreamHandler($config["file"], $config["level"]));
|
||||
|
|
|
@ -40,7 +40,6 @@ class StartSessionAction extends Action
|
|||
*/
|
||||
public function handle(array $inputs): array
|
||||
{
|
||||
$config = Config::get();
|
||||
$payload = [];
|
||||
|
||||
// Check if logged in
|
||||
|
@ -65,8 +64,8 @@ class StartSessionAction extends Action
|
|||
}
|
||||
|
||||
// Read global message
|
||||
if (isset($config["server"]["global_message"]) && trim($config["server"]["global_message"]) !== "")
|
||||
$payload["global_message"] = trim($config["server"]["global_message"]);
|
||||
if (Config::has("server.global_message") && trim(Config::get("server.global_message")) !== "")
|
||||
$payload["global_message"] = trim(Config::get("server.global_message"));
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
|
|
@ -76,4 +76,38 @@ class Util
|
|||
{
|
||||
return $interval - ((time() - $timestamp) / 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively merges arrays, while overwriting other types.
|
||||
*
|
||||
* Functions similar to `array_merge_recursive`, except that if two values are encountered at least one of which is not
|
||||
* an array, the value of `array2` is taken, instead of taking an array of both values.
|
||||
*
|
||||
* If a key exists in `array1` but not in `array2`, then the value of `array1` is used. If a key exists in `array2` but
|
||||
* not in `array1`, then the value of `array2` is used. If a key exists in both `array1` and `array2`, and both values
|
||||
* are arrays, this function is applied recursively, effectively using a merged array containing the values of both
|
||||
* arrays' arrays. If a key exists in both `array1` and `array2`, and at least one of the values is not an array, the
|
||||
* value of `array2` is used.
|
||||
*
|
||||
* Taken from `https://www.php.net/manual/en/function.array-merge-recursive.php#92195`.
|
||||
*
|
||||
* @param mixed[] $array1 the base array to merge into
|
||||
* @param mixed[] $array2 the array to merge into `array1`
|
||||
* @return mixed[] the recursively merged array
|
||||
* @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
|
||||
* @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
|
||||
*/
|
||||
static function array_merge_recursive_distinct(array $array1, array $array2): array
|
||||
{
|
||||
$merged = $array1;
|
||||
|
||||
foreach ($array2 as $key => $value)
|
||||
if (is_array($value) && isset ($merged[$key]) && is_array($merged[$key]))
|
||||
$merged[$key] = Util::array_merge_recursive_distinct($merged[$key], $value);
|
||||
else
|
||||
$merged[$key] = $value;
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ namespace com\fwdekker\deathnotifier\mailer;
|
|||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\UnexpectedException;
|
||||
use com\fwdekker\deathnotifier\validator\EqualsCliSecretRule;
|
||||
use com\fwdekker\deathnotifier\validator\EqualsCliPasswordRule;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use com\fwdekker\deathnotifier\validator\RuleSet;
|
||||
use PHPMailer\PHPMailer\Exception as PHPMailerException;
|
||||
|
@ -45,7 +45,7 @@ class ProcessEmailQueueAction extends Action
|
|||
*/
|
||||
public function handle(array $inputs): mixed
|
||||
{
|
||||
(new RuleSet(["password" => [new EqualsCliSecretRule()]]))->check($inputs);
|
||||
(new RuleSet(["password" => [new EqualsCliPasswordRule()]]))->check($inputs);
|
||||
|
||||
$mailer = $this->create_mailer();
|
||||
$emails = $this->email_queue->get_queue();
|
||||
|
@ -76,7 +76,7 @@ class ProcessEmailQueueAction extends Action
|
|||
*/
|
||||
private function create_mailer(): PHPMailer
|
||||
{
|
||||
$config = Config::get();
|
||||
$config = Config::get("mail");
|
||||
|
||||
$mailer = new PHPMailer();
|
||||
$mailer->IsSMTP();
|
||||
|
@ -86,12 +86,12 @@ class ProcessEmailQueueAction extends Action
|
|||
$mailer->SMTPDebug = SMTP::DEBUG_OFF;
|
||||
$mailer->SMTPKeepAlive = true;
|
||||
$mailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
||||
$mailer->Host = $config["mail"]["host"];
|
||||
$mailer->Port = $config["mail"]["port"];
|
||||
$mailer->Username = $config["mail"]["username"];
|
||||
$mailer->Password = $config["mail"]["password"];
|
||||
$mailer->Host = $config["host"];
|
||||
$mailer->Port = $config["port"];
|
||||
$mailer->Username = $config["username"];
|
||||
$mailer->Password = $config["password"];
|
||||
try {
|
||||
$mailer->setFrom($config["mail"]["username"], $config["mail"]["from_name"]);
|
||||
$mailer->setFrom($config["username"], $config["from_name"]);
|
||||
} catch (PHPMailerException $exception) {
|
||||
$mailer->smtpClose();
|
||||
throw new UnexpectedException(
|
||||
|
|
|
@ -8,7 +8,7 @@ use com\fwdekker\deathnotifier\LoggerUtil;
|
|||
use com\fwdekker\deathnotifier\mailer\Email;
|
||||
use com\fwdekker\deathnotifier\mailer\EmailQueue;
|
||||
use com\fwdekker\deathnotifier\UnexpectedException;
|
||||
use com\fwdekker\deathnotifier\validator\EqualsCliSecretRule;
|
||||
use com\fwdekker\deathnotifier\validator\EqualsCliPasswordRule;
|
||||
use com\fwdekker\deathnotifier\validator\InvalidInputException;
|
||||
use com\fwdekker\deathnotifier\validator\RuleSet;
|
||||
use com\fwdekker\deathnotifier\wikipedia\Wikipedia;
|
||||
|
@ -70,7 +70,7 @@ class UpdateTrackingsAction extends Action
|
|||
*/
|
||||
public function handle(array $inputs): mixed
|
||||
{
|
||||
(new RuleSet(["password" => [new EqualsCliSecretRule()]]))->check($inputs);
|
||||
(new RuleSet(["password" => [new EqualsCliPasswordRule()]]))->check($inputs);
|
||||
|
||||
// Process changes
|
||||
$new_deletions = [];
|
||||
|
@ -165,7 +165,7 @@ class NotifyStatusChangedEmail extends Email
|
|||
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
$base_path = Config::get("server.base_path");
|
||||
|
||||
return
|
||||
"Someone has edited Wikipedia to state that $this->name is $this->new_status. " .
|
||||
|
@ -219,7 +219,7 @@ class NotifyArticleDeletedEmail extends Email
|
|||
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
$base_path = Config::get("server.base_path");
|
||||
|
||||
return
|
||||
"The Wikipedia article about $this->name has been deleted. " .
|
||||
|
@ -274,7 +274,7 @@ class NotifyArticleUndeletedEmail extends Email
|
|||
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
$base_path = Config::get("server.base_path");
|
||||
|
||||
return
|
||||
"The Wikipedia article about $this->name has been re-created. " .
|
||||
|
|
|
@ -126,7 +126,7 @@ class ChangeEmailFromEmail extends Email
|
|||
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
$base_path = Config::get("server.base_path");
|
||||
|
||||
return
|
||||
"You changed the email address of your Death Notifier account from $this->recipient to $this->new_email. " .
|
||||
|
@ -186,7 +186,7 @@ class ChangeEmailToEmail extends Email
|
|||
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
$base_path = Config::get("server.base_path");
|
||||
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($this->recipient) . "&token=$this->token";
|
||||
|
||||
return
|
||||
|
|
|
@ -114,7 +114,7 @@ class ChangePasswordEmail extends Email
|
|||
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
$base_path = Config::get("server.base_path");
|
||||
|
||||
return
|
||||
"You changed the password of your Death Notifier account." .
|
||||
|
|
|
@ -115,7 +115,7 @@ class RegisterEmail extends Email
|
|||
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
$base_path = Config::get("server.base_path");
|
||||
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($this->recipient) . "&token=$this->token";
|
||||
|
||||
return
|
||||
|
|
|
@ -127,7 +127,7 @@ class ResendVerifyEmailEmail extends Email
|
|||
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
$base_path = Config::get("server.base_path");
|
||||
$verify_path = "$base_path?action=verify-email&email=" . rawurlencode($this->recipient) . "&token=$this->token";
|
||||
|
||||
return
|
||||
|
|
|
@ -118,7 +118,7 @@ class ResetPasswordEmail extends Email
|
|||
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
$base_path = Config::get("server.base_path");
|
||||
|
||||
return
|
||||
"You requested a password reset and changed the password of your Death Notifier account." .
|
||||
|
|
|
@ -125,7 +125,7 @@ class SendPasswordResetEmail extends Email
|
|||
|
||||
public function get_body(): string
|
||||
{
|
||||
$base_path = Config::get()["server"]["base_path"];
|
||||
$base_path = Config::get("server.base_path");
|
||||
$verify_path =
|
||||
"$base_path?action=reset-password&email=" . rawurlencode($this->recipient) . "&token=$this->token";
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\validator;
|
||||
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\IllegalArgumentError;
|
||||
|
||||
|
||||
/**
|
||||
* Validates that the input equals the CLI password.
|
||||
*/
|
||||
class EqualsCliPasswordRule extends Rule
|
||||
{
|
||||
/**
|
||||
* The key in the configuration at which the hash of the CLI password is stored.
|
||||
*/
|
||||
public const CONFIG_KEY = "admin.cli_password";
|
||||
/**
|
||||
* The default value of the CLI password.
|
||||
*/
|
||||
public const DEFAULT = "REPLACE THIS WITH A SECRET VALUE";
|
||||
|
||||
|
||||
/**
|
||||
* Validates that the input equals the CLI password.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
|
||||
* @param string $key the key in {@see $inputs} of the input to check
|
||||
* @return void if the checked input equals the CLI password
|
||||
* @throws InvalidInputException if the checked input does not equal the CLI password
|
||||
* @throws IllegalArgumentError if the CLI password is a blank string, if the CLI password is at its default value,
|
||||
* if the checked input is not set
|
||||
*/
|
||||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
if (!Config::has(self::CONFIG_KEY) || trim(Config::get(self::CONFIG_KEY)) === "")
|
||||
throw new IllegalArgumentError("The CLI is disabled because the CLI password is not set.");
|
||||
|
||||
if (Config::get(self::CONFIG_KEY) === self::DEFAULT)
|
||||
throw new IllegalArgumentError("The CLI is disabled because the CLI password is set to the default.");
|
||||
|
||||
if (!isset($inputs[$key]))
|
||||
throw new InvalidInputException("This operation requires the CLI password.", $key);
|
||||
|
||||
if (!password_verify($inputs[$key], Config::get(self::CONFIG_KEY)))
|
||||
throw new InvalidInputException("Incorrect CLI password.", $key);
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\validator;
|
||||
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
|
||||
|
||||
/**
|
||||
* Validates that the input equals the CLI secret.
|
||||
*/
|
||||
class EqualsCliSecretRule extends Rule
|
||||
{
|
||||
/**
|
||||
* The default value of the CLI secret.
|
||||
*/
|
||||
private const CLI_SECRET_DEFAULT = "REPLACE THIS WITH A SECRET VALUE";
|
||||
|
||||
|
||||
/**
|
||||
* Validates that the input equals the CLI secret.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
|
||||
* @param string $key the key in {@see $inputs} of the input to check
|
||||
* @return void if the checked input equals the CLI secret
|
||||
* @throws InvalidInputException if the checked input is not set, the CLI secret is a blank string, the CLI secret
|
||||
* is at its default value, or if the checked input does not equal the CLI secret
|
||||
*/
|
||||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
$cli_password = Config::get()["admin"]["cli_secret"];
|
||||
|
||||
if (trim($cli_password) === "" || $cli_password === self::CLI_SECRET_DEFAULT)
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ?? "The CLI is disabled because the CLI secret is not set.",
|
||||
$key
|
||||
);
|
||||
|
||||
if (!isset($inputs[$key]) || !password_verify($inputs[$key], Config::get()["admin"]["cli_secret"]))
|
||||
throw new InvalidInputException($this->override_message ?? "Incorrect password.", $key);
|
||||
}
|
||||
}
|
|
@ -23,13 +23,9 @@ class HasStringLengthRule extends Rule
|
|||
*
|
||||
* @param int|null $min_length the minimum length (inclusive), or `null` if there is no minimum length
|
||||
* @param int|null $max_length the maximum length (inclusive), or `null` if there is no maximum length
|
||||
* @param string|null $override_message the message to return if the rule does not apply to some input. If `null`,
|
||||
* the rule implementation can choose an appropriate message
|
||||
*/
|
||||
public function __construct(?int $min_length = null, ?int $max_length = null, ?string $override_message = null)
|
||||
public function __construct(?int $min_length = null, ?int $max_length = null)
|
||||
{
|
||||
parent::__construct($override_message);
|
||||
|
||||
$this->min_length = $min_length;
|
||||
$this->max_length = $max_length;
|
||||
}
|
||||
|
@ -46,20 +42,13 @@ class HasStringLengthRule extends Rule
|
|||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
if (!isset($inputs[$key]))
|
||||
throw new InvalidInputException($this->override_message ?? "Missing input '$key'.", $key);
|
||||
|
||||
throw new InvalidInputException("Missing input '$key'.", $key);
|
||||
if (!is_string($inputs[$key]))
|
||||
throw new InvalidInputException($this->override_message ?? "Field must be a string.", $key);
|
||||
throw new InvalidInputException("Field must be a string.", $key);
|
||||
|
||||
if ($this->min_length !== null && strlen($inputs[$key]) < $this->min_length)
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ?? "Use at least $this->min_length character(s).",
|
||||
$key
|
||||
);
|
||||
throw new InvalidInputException("Use at least $this->min_length character(s).", $key);
|
||||
if ($this->max_length !== null && strlen($inputs[$key]) > $this->max_length)
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ?? "Use at most $this->max_length character(s).",
|
||||
$key
|
||||
);
|
||||
throw new InvalidInputException("Use at most $this->max_length character(s).", $key);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,6 @@ class IsBooleanRule extends Rule
|
|||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
if (!isset($inputs[$key]) || !is_bool($inputs[$key]))
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ?? "Field '" . htmlentities($key) . "' must be a boolean.",
|
||||
$key
|
||||
);
|
||||
throw new InvalidInputException("Field '" . htmlentities($key) . "' must be a boolean.", $key);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\validator;
|
||||
|
||||
use com\fwdekker\deathnotifier\IllegalArgumentError;
|
||||
|
||||
|
||||
/**
|
||||
* Validates that the input is an email address.
|
||||
|
@ -14,11 +16,28 @@ class IsEmailRule extends Rule
|
|||
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
|
||||
* @param string $key the key in {@see $inputs} of the input to check
|
||||
* @return void if the checked input is an email address
|
||||
* @throws InvalidInputException if the checked input is not set, is not a string, or is not an email address
|
||||
* @throws InvalidInputException if the checked input is not an email address
|
||||
* @throws IllegalArgumentError if the checked input is not set or is not a string
|
||||
*/
|
||||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
if (!isset($inputs[$key]) || !is_string($inputs[$key]) || !filter_var($inputs[$key], FILTER_VALIDATE_EMAIL))
|
||||
throw new InvalidInputException($this->override_message ?? "Enter a valid email address.", $key);
|
||||
if (!isset($inputs[$key]))
|
||||
throw new IllegalArgumentError("Required input '$key' not set.");
|
||||
if (!is_string($inputs[$key]))
|
||||
throw new IllegalArgumentError("Input '$key' should be string, but is " . gettype($inputs[$key]) . ".");
|
||||
|
||||
$input = $inputs[$key];
|
||||
if (str_starts_with($input, " "))
|
||||
throw new InvalidInputException("Remove the spaces at the start.", $key);
|
||||
if (str_ends_with($input, " "))
|
||||
throw new InvalidInputException("Remove the spaces at the end.", $key);
|
||||
if ($input === "")
|
||||
throw new InvalidInputException("Enter an email address.", $key);
|
||||
if (!str_contains($input, "@"))
|
||||
throw new InvalidInputException("Don't forget to add an '@' symbol.", $key);
|
||||
if (str_ends_with($input, "@"))
|
||||
throw new InvalidInputException("Add a domain name after the '@'.", $key);
|
||||
if (!filter_var($input, FILTER_VALIDATE_EMAIL))
|
||||
throw new InvalidInputException("Enter a valid email address.", $key);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\validator;
|
||||
|
||||
|
||||
/**
|
||||
* Validates that the input has the specified value.
|
||||
*/
|
||||
class IsEqualToRule extends Rule
|
||||
{
|
||||
/**
|
||||
* @var string the required value
|
||||
*/
|
||||
private readonly string $expected;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new `IsEqualToRule`.
|
||||
*
|
||||
* @param string $expected the value that checked values should be equal to
|
||||
* @param string|null $override_message the message to return if the rule does not apply to some input. If `null`,
|
||||
* the rule implementation can choose an appropriate message
|
||||
*/
|
||||
public function __construct(string $expected, ?string $override_message = null)
|
||||
{
|
||||
parent::__construct($override_message);
|
||||
|
||||
$this->expected = $expected;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates that the input has the specified value.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
|
||||
* @param string $key the key in {@see $inputs} of the input to check
|
||||
* @return void if the checked input equals {@see $expected}
|
||||
* @throws InvalidInputException if the checked input is not set or does not equal {@see $expected}
|
||||
*/
|
||||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
if (!isset($inputs[$key]) || $inputs[$key] !== $this->expected)
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ?? "Input '$key' should equal '$this->expected'.",
|
||||
$key
|
||||
);
|
||||
}
|
||||
}
|
|
@ -19,6 +19,6 @@ class IsNotBlankRule extends Rule
|
|||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
if (!isset($inputs[$key]) || !is_string($inputs[$key]) || trim($inputs[$key]) === "")
|
||||
throw new InvalidInputException($this->override_message ?? "Use at least one character.", $key);
|
||||
throw new InvalidInputException("Use at least one character.", $key);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,24 @@ namespace com\fwdekker\deathnotifier\validator;
|
|||
*/
|
||||
class IsNotSetRule extends Rule
|
||||
{
|
||||
/**
|
||||
* @var string|null the message to return if the input is not set, or `null` to show the default message
|
||||
*/
|
||||
public readonly ?string $override_message;
|
||||
|
||||
|
||||
/**
|
||||
* Instantiates a new `IsSetRule`.
|
||||
*
|
||||
* @param string|null $override_message the message to return if the input is not set, or `null` to show the default
|
||||
* message
|
||||
*/
|
||||
public function __construct(?string $override_message = null)
|
||||
{
|
||||
$this->override_message = $override_message;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates that the input is not set.
|
||||
*
|
||||
|
|
|
@ -8,6 +8,24 @@ namespace com\fwdekker\deathnotifier\validator;
|
|||
*/
|
||||
class IsSetRule extends Rule
|
||||
{
|
||||
/**
|
||||
* @var string|null the message to return if the input is not set, or `null` to show the default message
|
||||
*/
|
||||
public readonly ?string $override_message;
|
||||
|
||||
|
||||
/**
|
||||
* Instantiates a new `IsSetRule`.
|
||||
*
|
||||
* @param string|null $override_message the message to return if the input is not set, or `null` to show the default
|
||||
* message
|
||||
*/
|
||||
public function __construct(?string $override_message = null)
|
||||
{
|
||||
$this->override_message = $override_message;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates that the input is set.
|
||||
*
|
||||
|
|
|
@ -19,9 +19,6 @@ class IsStringRule extends Rule
|
|||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
if (!isset($inputs[$key]) || !is_string($inputs[$key]))
|
||||
throw new InvalidInputException(
|
||||
$this->override_message ?? "Field '" . htmlentities($key) . "' must be a string.",
|
||||
$key
|
||||
);
|
||||
throw new InvalidInputException("Field '" . htmlentities($key) . "' must be a string.", $key);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,21 +4,21 @@ namespace com\fwdekker\deathnotifier\validator;
|
|||
|
||||
|
||||
/**
|
||||
* Validates that the input equals the CLI password.
|
||||
* Validates that the input is a valid CSRF token.
|
||||
*/
|
||||
class IsValidCsrfTokenRule extends IsEqualToRule
|
||||
class IsValidCsrfTokenRule extends Rule
|
||||
{
|
||||
/**
|
||||
* Constructs a new `IsValidCsrfTokenRule`.
|
||||
* Validates that the input is a valid CSRF token.
|
||||
*
|
||||
* @param string|null $override_message the message to return if the rule does not apply to some input. If `null`,
|
||||
* the rule implementation can choose an appropriate message
|
||||
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
|
||||
* @param string $key the key in {@see $inputs} of the input to check
|
||||
* @return void if the checked input is a valid CSRF token
|
||||
* @throws InvalidInputException if the checked input is not a valid CSRF token
|
||||
*/
|
||||
public function __construct(?string $override_message = null)
|
||||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
parent::__construct(
|
||||
$_SESSION["token"],
|
||||
$override_message ?? "Invalid request token. Please refresh the page and try again."
|
||||
);
|
||||
if (!isset($inputs[$key]) || $inputs[$key] !== $_SESSION["token"])
|
||||
throw new InvalidInputException("Invalid request token. Please refresh the page and try again.", $key);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,40 +2,22 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\validator;
|
||||
|
||||
use com\fwdekker\deathnotifier\IllegalArgumentError;
|
||||
|
||||
|
||||
/**
|
||||
* A rule/constraint/assertion that should hold over an input.
|
||||
*/
|
||||
abstract class Rule
|
||||
{
|
||||
/**
|
||||
* @var string|null the message to return if the rule does not apply to some input. If `null`, the rule
|
||||
* implementation can choose an appropriate message.
|
||||
*/
|
||||
public readonly ?string $override_message;
|
||||
|
||||
|
||||
/**
|
||||
* Instantiates a new rule.
|
||||
*
|
||||
* @param string|null $override_message the message to return if the rule does not apply to some input. If `null`,
|
||||
* the rule implementation can choose an appropriate message
|
||||
*/
|
||||
public function __construct(?string $override_message = null)
|
||||
{
|
||||
$this->override_message = $override_message;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether the rule holds for `$inputs[$key]`.
|
||||
*
|
||||
* Implementations should never assume that the `$inputs[$key]` is set.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param string $key the key in `inputs` of the input to check
|
||||
* @return void if the rule holds
|
||||
* @throws InvalidInputException if the rule does not hold
|
||||
* @param array<int|string, mixed> $inputs the list of inputs in which the value at {@see $key} should be checked
|
||||
* @param string $key the key in {@see $inputs} of the input to check
|
||||
* @return void if the rule holds for the checked input
|
||||
* @throws InvalidInputException if the rule does not hold for the checked input
|
||||
* @throws IllegalArgumentError if the checked input is not set or of the wrong type
|
||||
*/
|
||||
public abstract function check(array $inputs, string $key): void;
|
||||
}
|
||||
|
|
|
@ -24,9 +24,11 @@ class SessionRuleSet extends RuleSet
|
|||
|
||||
$rules = [];
|
||||
if ($validate_logged_in)
|
||||
$rules["uuid"] = [new IsSetRule("You must be logged in to perform this action.")];
|
||||
$rules["uuid"] =
|
||||
[new IsSetRule("You must be logged in to perform this action. Refresh the page and try again.")];
|
||||
if ($validate_logged_out)
|
||||
$rules["uuid"] = [new IsNotSetRule("You must be logged out to perform this action.")];
|
||||
$rules["uuid"] =
|
||||
[new IsNotSetRule("You must be logged out to perform this action. Refresh the page and try again.")];
|
||||
|
||||
parent::__construct($rules);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\wikipedia;
|
||||
|
||||
use com\fwdekker\deathnotifier\Util;
|
||||
use JsonException;
|
||||
|
||||
|
||||
|
@ -81,7 +82,7 @@ class Wikipedia
|
|||
: array_merge($query_params, ["continue" => $continue, $continue_name => $continue_value]);
|
||||
|
||||
$new_response = $this->api_fetch($continue_params);
|
||||
$response = array_merge_recursive_distinct($response, $new_response);
|
||||
$response = Util::array_merge_recursive_distinct($response, $new_response);
|
||||
|
||||
if (isset($response["batchcomplete"])) {
|
||||
$continue = null;
|
||||
|
@ -219,37 +220,3 @@ class Wikipedia
|
|||
return new QueryOutput($articles, $output->redirects, $output->missing);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursively merges arrays, while overwriting other types.
|
||||
*
|
||||
* Functions similar to `array_merge_recursive`, except that if two values are encountered at least one of which is not
|
||||
* an array, the value of `array2` is taken, instead of taking an array of both values.
|
||||
*
|
||||
* If a key exists in `array1` but not in `array2`, then the value of `array1` is used. If a key exists in `array2` but
|
||||
* not in `array1`, then the value of `array2` is used. If a key exists in both `array1` and `array2`, and both values
|
||||
* are arrays, this function is applied recursively, effectively using a merged array containing the values of both
|
||||
* arrays' arrays. If a key exists in both `array1` and `array2`, and at least one of the values is not an array, the
|
||||
* value of `array2` is used.
|
||||
*
|
||||
* Taken from `https://www.php.net/manual/en/function.array-merge-recursive.php#92195`.
|
||||
*
|
||||
* @param mixed[] $array1 the base array to merge into
|
||||
* @param mixed[] $array2 the array to merge into `array1`
|
||||
* @return mixed[] the recursively merged array
|
||||
* @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
|
||||
* @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
|
||||
*/
|
||||
function array_merge_recursive_distinct(array $array1, array $array2): array
|
||||
{
|
||||
$merged = $array1;
|
||||
|
||||
foreach ($array2 as $key => $value)
|
||||
if (is_array($value) && isset ($merged[$key]) && is_array($merged[$key]))
|
||||
$merged[$key] = array_merge_recursive_distinct($merged[$key], $value);
|
||||
else
|
||||
$merged[$key] = $value;
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\validator;
|
||||
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for {@see Config}.
|
||||
*/
|
||||
class ConfigTest extends TestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
{
|
||||
Config::_reset();
|
||||
}
|
||||
|
||||
|
||||
public function test_has_returns_false_if_section_does_not_exist(): void
|
||||
{
|
||||
self::assertFalse(Config::has("test_section"));
|
||||
}
|
||||
|
||||
public function test_has_returns_false_if_property_does_not_exist(): void
|
||||
{
|
||||
self::assertFalse(Config::has("test_section.property"));
|
||||
}
|
||||
|
||||
|
||||
public function test_set_creates_section(): void
|
||||
{
|
||||
Config::_set("test_section", "value");
|
||||
|
||||
self::assertTrue(Config::has("test_section"));
|
||||
}
|
||||
|
||||
public function test_set_creates_property_in_section(): void
|
||||
{
|
||||
Config::_set("test_section.property", "value");
|
||||
|
||||
self::assertTrue(Config::has("test_section.property"));
|
||||
}
|
||||
|
||||
public function test_set_creates_section_for_property(): void
|
||||
{
|
||||
Config::_set("test_section.property", "value");
|
||||
|
||||
self::assertTrue(Config::has("test_section"));
|
||||
}
|
||||
|
||||
public function test_set_adds_property_to_existing_section(): void
|
||||
{
|
||||
Config::_set("test_section.property1", "value");
|
||||
Config::_set("test_section.property2", "value");
|
||||
|
||||
self::assertTrue(Config::has("test_section.property1"));
|
||||
self::assertTrue(Config::has("test_section.property2"));
|
||||
}
|
||||
|
||||
public function test_set_overwrites_existing_property(): void
|
||||
{
|
||||
Config::_set("test_section.property", "value");
|
||||
|
||||
Config::_set("test_section.property", "new_value");
|
||||
|
||||
self::assertEquals("new_value", Config::get("test_section.property"));
|
||||
}
|
||||
|
||||
|
||||
public function test_get_returns_all_with_null_index(): void
|
||||
{
|
||||
Config::_set("test_section.property", "value");
|
||||
|
||||
$config = Config::get();
|
||||
self::assertArrayHasKey("test_section", $config);
|
||||
self::assertIsArray($config["test_section"]);
|
||||
}
|
||||
|
||||
public function test_get_returns_section(): void
|
||||
{
|
||||
Config::_set("test_section.property", "value");
|
||||
|
||||
self::assertEquals(["property" => "value"], Config::get("test_section"));
|
||||
}
|
||||
|
||||
public function test_get_returns_property(): void
|
||||
{
|
||||
Config::_set("test_section.property", "value");
|
||||
|
||||
self::assertEquals("value", Config::get("test_section.property"));
|
||||
}
|
||||
|
||||
|
||||
public function test_reset(): void
|
||||
{
|
||||
Config::_set("test_section.property", "value");
|
||||
|
||||
Config::_reset();
|
||||
|
||||
self::assertFalse(Config::has("test_section.property"));
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ use com\fwdekker\deathnotifier\mailer\EmailQueue;
|
|||
use com\fwdekker\deathnotifier\tracking\TrackingList;
|
||||
use com\fwdekker\deathnotifier\user\UserList;
|
||||
use Exception;
|
||||
use Monolog\Test\TestCase;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\validator;
|
||||
|
||||
use com\fwdekker\deathnotifier\Config;
|
||||
use com\fwdekker\deathnotifier\IllegalArgumentError;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Throwable;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for {@see EqualsCliPasswordRule}.
|
||||
*/
|
||||
class EqualsCliPasswordRuleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Tests the output of {@see EqualsCliPasswordRule::check()}.
|
||||
*
|
||||
* @param string $name the name to give to the test case
|
||||
* @param string|null $password the password that is set in the configuration
|
||||
* @param string|null $input the user's input
|
||||
* @param class-string<Throwable>|null $exception the exception that is asserted to be thrown
|
||||
* @param string|null $exception_message the exception message that is asserted
|
||||
* @return void
|
||||
* @throws InvalidInputException if {@see $exception} is `null` but an exception is thrown
|
||||
* @dataProvider check_provider
|
||||
*/
|
||||
public function test_check(string $name, ?string $password, ?string $input, ?string $exception,
|
||||
?string $exception_message): void
|
||||
{
|
||||
self::setName($name);
|
||||
|
||||
if ($exception !== null)
|
||||
self::expectException($exception);
|
||||
if ($exception_message !== null)
|
||||
self::expectExceptionMessage($exception_message);
|
||||
|
||||
Config::_set(EqualsCliPasswordRule::CONFIG_KEY, $password);
|
||||
(new EqualsCliPasswordRule())->check(["key" => $input], "key");
|
||||
Config::_reset();
|
||||
|
||||
if ($exception === null && $exception_message === null)
|
||||
self::assertTrue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the test cases.
|
||||
*
|
||||
* @return array<array{string, string|null, string|null, class-string<Throwable>|null, string|null}> the test cases
|
||||
* @see RuleTest::test_check()
|
||||
*/
|
||||
public function check_provider(): array
|
||||
{
|
||||
$hash = "\$2y\$04\$fwXTw7Rjzw0EpU094u4agOBaBNqtCHGc4TMoxfbPrxuqO5tpYyRka"; # Hash of "password"
|
||||
|
||||
$error = IllegalArgumentError::class;
|
||||
$exception = InvalidInputException::class;
|
||||
|
||||
return [
|
||||
["error if password is not set", null, "input", $error, "The CLI is disabled because the CLI password is not set."],
|
||||
["error if password is blank", " ", "input", $error, "The CLI is disabled because the CLI password is not set."],
|
||||
["error if password is default", EqualsCliPasswordRule::DEFAULT, "input", $error, "The CLI is disabled because the CLI password is set to the default."],
|
||||
["exception if input is not set", $hash, null, $exception, "This operation requires the CLI password."],
|
||||
["exception if input is incorrect", $hash, "incorrect", $exception, "Incorrect CLI password."],
|
||||
["no exception if input is correct", $hash, "password", null, null],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -2,44 +2,30 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\validator;
|
||||
|
||||
use com\fwdekker\deathnotifier\IllegalArgumentError;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for `IsEmailRule`.
|
||||
* Unit tests for {@see IsEmailRule}.
|
||||
*/
|
||||
class IsEmailRuleTest extends RuleTest
|
||||
{
|
||||
function get_rule(?string $override = null): Rule
|
||||
public function check_provider(): array
|
||||
{
|
||||
return new IsEmailRule($override);
|
||||
}
|
||||
$error = IllegalArgumentError::class;
|
||||
$exception = InvalidInputException::class;
|
||||
|
||||
function get_valid_input(): ?string
|
||||
{
|
||||
return "test@test.test";
|
||||
}
|
||||
|
||||
function get_invalid_input(): ?string
|
||||
{
|
||||
return "invalid";
|
||||
}
|
||||
|
||||
|
||||
public function test_returns_null_if_email_is_valid(): void
|
||||
{
|
||||
$rule = new IsEmailRule();
|
||||
|
||||
$is_valid = $rule->check(["email" => "example@example.com"], "email");
|
||||
|
||||
self::assertNull($is_valid);
|
||||
}
|
||||
|
||||
public function test_returns_response_message_if_email_is_invalid(): void
|
||||
{
|
||||
$rule = new IsEmailRule();
|
||||
|
||||
$is_valid = $rule->check(["email" => "example.com"], "email");
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
self::assertEquals("Enter a valid email address.", $is_valid->payload["message"]);
|
||||
return [
|
||||
["error if input is not set", null, $error, "Required input 'key' not set."],
|
||||
["error if input is not string", 491, $error, "Input 'key' should be string, but is integer."],
|
||||
["exception if input starts with space", " valid@example.com", $exception, "Remove the spaces at the start."],
|
||||
["exception if input ends with space", "valid@example.com ", $exception, "Remove the spaces at the end."],
|
||||
["exception if input is empty", "", $exception, "Enter an email address."],
|
||||
["exception if input misses the '@' symbol", "invalid", $exception, "Don't forget to add an '@' symbol."],
|
||||
["exception if input misses the domain part", "invalid@", $exception, "Add a domain name after the '@'."],
|
||||
["exception if input is invalid in a complex way", "invalid@example", $exception, "Enter a valid email address."],
|
||||
["no exception if input is valid", "valid@example.com", null, null],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\validator;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for `IsNotBlankRule`.
|
||||
*/
|
||||
class IsNotBlankRuleTest extends RuleTest
|
||||
{
|
||||
function get_rule(?string $override = null): Rule
|
||||
{
|
||||
return new IsNotBlankRule($override);
|
||||
}
|
||||
|
||||
function get_valid_input(): ?string
|
||||
{
|
||||
return "not-blank";
|
||||
}
|
||||
|
||||
function get_invalid_input(): ?string
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
public function test_returns_null_if_string_is_not_blank(): void
|
||||
{
|
||||
$rule = new IsNotBlankRule();
|
||||
|
||||
$is_valid = $rule->check(["input" => "not-blank"], "input");
|
||||
|
||||
self::assertNull($is_valid);
|
||||
}
|
||||
|
||||
public function test_returns_response_message_if_input_is_the_empty_string(): void
|
||||
{
|
||||
$rule = new IsNotBlankRule();
|
||||
|
||||
$is_valid = $rule->check(["input" => ""], "input");
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
self::assertEquals("Use at least one character.", $is_valid->payload["message"]);
|
||||
}
|
||||
|
||||
public function test_returns_response_message_if_input_contains_whitespace_only(): void
|
||||
{
|
||||
$rule = new IsNotBlankRule();
|
||||
|
||||
$is_valid = $rule->check(["input" => " "], "input");
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
self::assertEquals("Use at least one character.", $is_valid->payload["message"]);
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\validator;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for `IsSetRule`.
|
||||
*/
|
||||
class IsSetRuleTest extends RuleTest
|
||||
{
|
||||
function get_rule(?string $override = null): Rule
|
||||
{
|
||||
return new IsSetRule($override);
|
||||
}
|
||||
|
||||
function get_valid_input(): ?string
|
||||
{
|
||||
return "is-set";
|
||||
}
|
||||
|
||||
function get_invalid_input(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\validator;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for `HasLengthRule`.
|
||||
*/
|
||||
class LengthRuleTest extends RuleTest
|
||||
{
|
||||
function get_rule(?string $override = null): Rule
|
||||
{
|
||||
return new HasStringLengthRule(1, 6, $override);
|
||||
}
|
||||
|
||||
function get_valid_input(): ?string
|
||||
{
|
||||
return "valid";
|
||||
}
|
||||
|
||||
function get_invalid_input(): ?string
|
||||
{
|
||||
return "too-long";
|
||||
}
|
||||
|
||||
|
||||
public function test_returns_null_if_input_is_exactly_minimum_length(): void
|
||||
{
|
||||
$rule = new HasStringLengthRule(1, 3);
|
||||
|
||||
$is_valid = $rule->check(["input" => "a"], "input");
|
||||
|
||||
self::assertNull($is_valid);
|
||||
}
|
||||
|
||||
public function test_returns_null_if_input_is_exactly_maximum_length(): void
|
||||
{
|
||||
$rule = new HasStringLengthRule(1, 3);
|
||||
|
||||
$is_valid = $rule->check(["input" => "123"], "input");
|
||||
|
||||
self::assertNull($is_valid);
|
||||
}
|
||||
|
||||
public function test_returns_null_if_input_is_strictly_inside_range(): void
|
||||
{
|
||||
$rule = new HasStringLengthRule(1, 3);
|
||||
|
||||
$is_valid = $rule->check(["input" => "12"], "input");
|
||||
|
||||
self::assertNull($is_valid);
|
||||
}
|
||||
|
||||
public function test_returns_not_null_if_input_is_strictly_below_minimum(): void
|
||||
{
|
||||
$rule = new HasStringLengthRule(1, 3);
|
||||
|
||||
$is_valid = $rule->check(["input" => ""], "input");
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
}
|
||||
|
||||
public function test_returns_not_null_if_input_is_strictly_above_maximum(): void
|
||||
{
|
||||
$rule = new HasStringLengthRule(1, 3);
|
||||
|
||||
$is_valid = $rule->check(["input" => "1234"], "input");
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
}
|
||||
}
|
|
@ -2,97 +2,47 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\validator;
|
||||
|
||||
use Monolog\Test\TestCase;
|
||||
use com\fwdekker\deathnotifier\IllegalArgumentError;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Throwable;
|
||||
|
||||
|
||||
/**
|
||||
* Inheritable unit tests for extensions of `Rule`.
|
||||
* Unit tests for {@see Rule} implementations.
|
||||
*/
|
||||
abstract class RuleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Constructs a new `Rule` of the class under test, using `override`.
|
||||
* Tests the output of {@see Rule::check()}.
|
||||
*
|
||||
* The returned `Rule` will only be tested under `get_valid_input` and `get_invalid_input`.
|
||||
*
|
||||
* @param string|null $override the override message
|
||||
* @return Rule a new `Rule` of the class under test, using `override`
|
||||
* @param string $name the name to give to the test case
|
||||
* @param mixed|null $input the user's input
|
||||
* @param class-string<Throwable>|null $exception the exception that is asserted to be thrown
|
||||
* @param string|null $exception_message the exception message that is asserted
|
||||
* @return void
|
||||
* @throws InvalidInputException if {@see $exception} is `null` but an exception is thrown
|
||||
* @dataProvider check_provider
|
||||
*/
|
||||
abstract function get_rule(?string $override = null): Rule;
|
||||
public function test_check(string $name, mixed $input, ?string $exception, ?string $exception_message): void
|
||||
{
|
||||
self::setName($name);
|
||||
|
||||
if ($exception !== null)
|
||||
self::expectException($exception);
|
||||
if ($exception_message !== null)
|
||||
self::expectExceptionMessage($exception_message);
|
||||
|
||||
(new IsEmailRule())->check(["key" => $input], "key");
|
||||
|
||||
if ($exception === null)
|
||||
self::assertTrue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null a valid input to give to `get_rule`
|
||||
* Returns the test cases.
|
||||
*
|
||||
* @return array<array{string, mixed|null, class-string<Throwable>|null, string|null}> the test cases
|
||||
* @see RuleTest::test_check()
|
||||
*/
|
||||
abstract function get_valid_input(): ?string;
|
||||
|
||||
/**
|
||||
* @return string|null an invalid input to give to `get_rule`
|
||||
*/
|
||||
abstract function get_invalid_input(): ?string;
|
||||
|
||||
|
||||
public function test_returns_null_if_input_is_valid(): void
|
||||
{
|
||||
$rule = $this->get_rule();
|
||||
|
||||
$is_valid = $rule->check(["input" => $this->get_valid_input()], "input");
|
||||
|
||||
self::assertNull($is_valid);
|
||||
}
|
||||
|
||||
public function test_returns_not_null_if_input_is_invalid(): void
|
||||
{
|
||||
$rule = $this->get_rule();
|
||||
|
||||
$is_valid = $rule->check(["input" => $this->get_invalid_input()], "input");
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
}
|
||||
|
||||
public function test_returns_not_null_if_input_is_not_set(): void
|
||||
{
|
||||
$rule = $this->get_rule();
|
||||
|
||||
$is_valid = $rule->check(["input" => $this->get_valid_input()], "does_not_exist");
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
}
|
||||
|
||||
public function test_returns_unsatisfied_payload_if_input_is_invalid(): void
|
||||
{
|
||||
$rule = $this->get_rule();
|
||||
|
||||
$is_valid = $rule->check(["input" => $this->get_invalid_input()], "input");
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
self::assertFalse($is_valid->satisfied);
|
||||
}
|
||||
|
||||
public function test_returns_payload_with_overridden_message_if_input_is_invalid(): void
|
||||
{
|
||||
$override = "Override message.";
|
||||
$rule = $this->get_rule($override);
|
||||
|
||||
$is_valid = $rule->check(["input" => $this->get_invalid_input()], "input");
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
self::assertEquals($override, $is_valid->payload["message"]);
|
||||
}
|
||||
|
||||
public function test_returns_payload_with_key_of_invalid_input_if_input_is_invalid(): void
|
||||
{
|
||||
$rule = $this->get_rule();
|
||||
|
||||
$is_valid = $rule->check(
|
||||
[
|
||||
"valid1" => $this->get_valid_input(),
|
||||
"invalid" => $this->get_invalid_input(),
|
||||
"valid2" => $this->get_valid_input(),
|
||||
],
|
||||
"invalid"
|
||||
);
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
self::assertEquals("invalid", $is_valid->payload["target"]);
|
||||
}
|
||||
abstract public function check_provider(): array;
|
||||
}
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\validator;
|
||||
|
||||
use Monolog\Test\TestCase;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for `Validator`.
|
||||
*/
|
||||
class ValidatorTest extends TestCase
|
||||
{
|
||||
function test_validate_inputs_returns_null_if_there_are_no_rule_sets(): void
|
||||
{
|
||||
$inputs = ["among" => "thief", "line" => "age"];
|
||||
$rule_sets = [];
|
||||
|
||||
$is_valid = Validator::validate_inputs($inputs, $rule_sets);
|
||||
|
||||
self::assertNull($is_valid);
|
||||
}
|
||||
|
||||
function test_validate_inputs_returns_null_if_all_inputs_are_valid(): void
|
||||
{
|
||||
$inputs = ["among" => "thief", "line" => "age"];
|
||||
$rule_sets = [
|
||||
"among" => [new IsSetRule(), new IsNotBlankRule()],
|
||||
"line" => [new IsSetRule()]
|
||||
];
|
||||
|
||||
$is_valid = Validator::validate_inputs($inputs, $rule_sets);
|
||||
|
||||
self::assertNull($is_valid);
|
||||
}
|
||||
|
||||
function test_validate_inputs_considers_empty_rule_set_to_always_be_valid(): void
|
||||
{
|
||||
$inputs = ["among" => "thief", "line" => "age"];
|
||||
$rule_sets = ["among" => []];
|
||||
|
||||
$is_valid = Validator::validate_inputs($inputs, $rule_sets);
|
||||
|
||||
self::assertNull($is_valid);
|
||||
}
|
||||
|
||||
function test_validate_inputs_returns_not_null_if_at_least_one_rule_is_violated(): void
|
||||
{
|
||||
$inputs = ["among" => "", "line" => "age"];
|
||||
$rule_sets = [
|
||||
"among" => [new IsSetRule(), new IsNotBlankRule()],
|
||||
"line" => [new IsSetRule()]
|
||||
];
|
||||
|
||||
$is_valid = Validator::validate_inputs($inputs, $rule_sets);
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
}
|
||||
|
||||
function test_validate_inputs_returns_first_violation_if_multiple_inputs_are_invalid(): void
|
||||
{
|
||||
$inputs = [];
|
||||
$rule_sets = [
|
||||
"among" => [new IsSetRule()],
|
||||
"line" => [new IsSetRule()]
|
||||
];
|
||||
|
||||
$is_valid = Validator::validate_inputs($inputs, $rule_sets);
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
self::assertEquals("among", $is_valid->payload["target"]);
|
||||
}
|
||||
|
||||
function test_validate_inputs_returns_first_violation_if_one_input_is_invalid_in_multiple_ways(): void
|
||||
{
|
||||
$inputs = ["line" => "age"];
|
||||
$rule_sets = [
|
||||
"among" => [new IsSetRule(), new IsNotBlankRule()],
|
||||
"line" => [new IsSetRule()]
|
||||
];
|
||||
|
||||
$is_valid = Validator::validate_inputs($inputs, $rule_sets);
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
self::assertEquals("among", $is_valid->payload["target"]);
|
||||
self::assertEquals("Field 'among' required.", $is_valid->payload["message"]);
|
||||
}
|
||||
|
||||
|
||||
function test_validate_logged_in_returns_null_if_uuid_is_set(): void
|
||||
{
|
||||
$session = ["uuid" => "value"];
|
||||
|
||||
$is_valid = Validator::validate_logged_in($session);
|
||||
|
||||
self::assertNull($is_valid);
|
||||
}
|
||||
|
||||
function test_validate_logged_in_returns_not_null_if_uuid_is_not_set(): void
|
||||
{
|
||||
$session = [];
|
||||
|
||||
$is_valid = Validator::validate_logged_in($session);
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
}
|
||||
|
||||
|
||||
function test_validate_logged_out_returns_not_null_if_uuid_is_set(): void
|
||||
{
|
||||
$session = ["uuid" => "value"];
|
||||
|
||||
$is_valid = Validator::validate_logged_out($session);
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
}
|
||||
|
||||
function test_validate_logged_out_returns_null_if_uuid_is_not_set(): void
|
||||
{
|
||||
$session = [];
|
||||
|
||||
$is_valid = Validator::validate_logged_out($session);
|
||||
|
||||
self::assertNull($is_valid);
|
||||
}
|
||||
|
||||
|
||||
function test_validate_token_returns_null_if_token_equals_input(): void
|
||||
{
|
||||
$token = "meow";
|
||||
$array = ["token" => $token];
|
||||
|
||||
$is_valid = Validator::validate_token($array, $token);
|
||||
|
||||
self::assertNull($is_valid);
|
||||
}
|
||||
|
||||
function test_validate_token_returns_not_null_if_token_is_not_set(): void
|
||||
{
|
||||
$token = "meow";
|
||||
$array = [];
|
||||
|
||||
$is_valid = Validator::validate_token($array, $token);
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
}
|
||||
|
||||
function test_validate_token_returns_not_null_if_token_does_not_equal_input(): void
|
||||
{
|
||||
$token = "meow";
|
||||
$array = ["token" => $token];
|
||||
|
||||
$is_valid = Validator::validate_token($array, "woof");
|
||||
|
||||
self::assertNotNull($is_valid);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue