From 732298bf09f84663b239dabca2497c6074c7b224 Mon Sep 17 00:00:00 2001 From: "Florine W. Dekker" Date: Wed, 7 Dec 2022 23:12:33 +0100 Subject: [PATCH] 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. --- composer.json | 2 +- composer.lock | Bin 77292 -> 77292 bytes package-lock.json | Bin 226174 -> 226174 bytes package.json | 2 +- src/main/api.php | 2 +- src/main/config.default.ini.php | 2 +- .../php/com/fwdekker/deathnotifier/Config.php | 89 +++++++++- .../com/fwdekker/deathnotifier/LoggerUtil.php | 2 +- .../deathnotifier/StartSessionAction.php | 5 +- .../php/com/fwdekker/deathnotifier/Util.php | 34 ++++ .../mailer/ProcessEmailQueueAction.php | 16 +- .../tracking/UpdateTrackingsAction.php | 10 +- .../deathnotifier/user/ChangeEmailAction.php | 4 +- .../user/ChangePasswordAction.php | 2 +- .../deathnotifier/user/RegisterAction.php | 2 +- .../user/ResendVerifyEmailAction.php | 2 +- .../user/ResetPasswordAction.php | 2 +- .../user/SendPasswordResetAction.php | 2 +- .../validator/EqualsCliPasswordRule.php | 48 ++++++ .../validator/EqualsCliSecretRule.php | 41 ----- .../validator/HasStringLengthRule.php | 21 +-- .../deathnotifier/validator/IsBooleanRule.php | 5 +- .../deathnotifier/validator/IsEmailRule.php | 25 ++- .../deathnotifier/validator/IsEqualToRule.php | 48 ------ .../validator/IsNotBlankRule.php | 2 +- .../deathnotifier/validator/IsNotSetRule.php | 18 ++ .../deathnotifier/validator/IsSetRule.php | 18 ++ .../deathnotifier/validator/IsStringRule.php | 5 +- .../validator/IsValidCsrfTokenRule.php | 20 +-- .../fwdekker/deathnotifier/validator/Rule.php | 32 +--- .../validator/SessionRuleSet.php | 6 +- .../deathnotifier/wikipedia/Wikipedia.php | 37 +---- .../com/fwdekker/deathnotifier/ConfigTest.php | 103 ++++++++++++ .../deathnotifier/DatabaseTestCase.php | 2 +- .../validator/EqualsCliPasswordRuleTest.php | 68 ++++++++ .../validator/IsEmailRuleTest.php | 50 ++---- .../validator/IsNotBlankRuleTest.php | 55 ------ .../deathnotifier/validator/IsSetRuleTest.php | 25 --- .../validator/LengthRuleTest.php | 71 -------- .../deathnotifier/validator/RuleTest.php | 112 ++++--------- .../deathnotifier/validator/ValidatorTest.php | 156 ------------------ 41 files changed, 499 insertions(+), 647 deletions(-) create mode 100644 src/main/php/com/fwdekker/deathnotifier/validator/EqualsCliPasswordRule.php delete mode 100644 src/main/php/com/fwdekker/deathnotifier/validator/EqualsCliSecretRule.php delete mode 100644 src/main/php/com/fwdekker/deathnotifier/validator/IsEqualToRule.php create mode 100644 src/test/php/com/fwdekker/deathnotifier/ConfigTest.php create mode 100644 src/test/php/com/fwdekker/deathnotifier/validator/EqualsCliPasswordRuleTest.php delete mode 100644 src/test/php/com/fwdekker/deathnotifier/validator/IsNotBlankRuleTest.php delete mode 100644 src/test/php/com/fwdekker/deathnotifier/validator/IsSetRuleTest.php delete mode 100644 src/test/php/com/fwdekker/deathnotifier/validator/LengthRuleTest.php delete mode 100644 src/test/php/com/fwdekker/deathnotifier/validator/ValidatorTest.php diff --git a/composer.json b/composer.json index 40f4cc4..ebe6e3b 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 424e779e89e932fbc14cccdc0b6d13bccae9bb06..eb4a537b282e5c42f63dcb629540acab2c14e39f 100644 GIT binary patch delta 51 zcmaEJo8`@ImJPy;3YM0M1|~*^NofX&DTxL~NolDTY37M3MyVF2mZ=uTiOt50+l?6+ Hcj^ED&vp<} delta 51 zcmaEJo8`@ImJPy;3W+I}iN>a;sRo87X-1}omZ^zmCT0dENfv2IMwS+)sm;cW+l?6+ Hcj^ED!2S=J diff --git a/package-lock.json b/package-lock.json index 8039afcee4ce23fca87bd8e90db3e31b41e66ab7..2fdd55392b1870ce420f6c4aa877ea0751d08127 100644 GIT binary patch delta 36 qcmezOjQ8I&-U-IcmU;#g&8C2;$qRXf8y~bjU~GNBwDke=ydnS{Y7buk delta 36 qcmezOjQ8I&-U-Ic7J5b#&8C2;$qRXf8y~bjU~GNBwDke=ydnS{kq==2 diff --git a/package.json b/package.json index 7ee2ff9..72cb43d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/main/api.php b/src/main/api.php index 3aa6c43..f314c1f 100644 --- a/src/main/api.php +++ b/src/main/api.php @@ -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); diff --git a/src/main/config.default.ini.php b/src/main/config.default.ini.php index 1dea51f..073bf86 100644 --- a/src/main/config.default.ini.php +++ b/src/main/config.default.ini.php @@ -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. diff --git a/src/main/php/com/fwdekker/deathnotifier/Config.php b/src/main/php/com/fwdekker/deathnotifier/Config.php index 18ef6d4..5279cef 100644 --- a/src/main/php/com/fwdekker/deathnotifier/Config.php +++ b/src/main/php/com/fwdekker/deathnotifier/Config.php @@ -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 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 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"]) { diff --git a/src/main/php/com/fwdekker/deathnotifier/LoggerUtil.php b/src/main/php/com/fwdekker/deathnotifier/LoggerUtil.php index a70f3e0..fbb97e1 100644 --- a/src/main/php/com/fwdekker/deathnotifier/LoggerUtil.php +++ b/src/main/php/com/fwdekker/deathnotifier/LoggerUtil.php @@ -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"])); diff --git a/src/main/php/com/fwdekker/deathnotifier/StartSessionAction.php b/src/main/php/com/fwdekker/deathnotifier/StartSessionAction.php index 317832c..41b6869 100644 --- a/src/main/php/com/fwdekker/deathnotifier/StartSessionAction.php +++ b/src/main/php/com/fwdekker/deathnotifier/StartSessionAction.php @@ -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; } diff --git a/src/main/php/com/fwdekker/deathnotifier/Util.php b/src/main/php/com/fwdekker/deathnotifier/Util.php index 51641c2..dd206c9 100644 --- a/src/main/php/com/fwdekker/deathnotifier/Util.php +++ b/src/main/php/com/fwdekker/deathnotifier/Util.php @@ -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 + * @author Gabriel Sobrinho + */ + 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; + } + } diff --git a/src/main/php/com/fwdekker/deathnotifier/mailer/ProcessEmailQueueAction.php b/src/main/php/com/fwdekker/deathnotifier/mailer/ProcessEmailQueueAction.php index a11e7db..4440c93 100644 --- a/src/main/php/com/fwdekker/deathnotifier/mailer/ProcessEmailQueueAction.php +++ b/src/main/php/com/fwdekker/deathnotifier/mailer/ProcessEmailQueueAction.php @@ -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( diff --git a/src/main/php/com/fwdekker/deathnotifier/tracking/UpdateTrackingsAction.php b/src/main/php/com/fwdekker/deathnotifier/tracking/UpdateTrackingsAction.php index 11b0b20..df0ad32 100644 --- a/src/main/php/com/fwdekker/deathnotifier/tracking/UpdateTrackingsAction.php +++ b/src/main/php/com/fwdekker/deathnotifier/tracking/UpdateTrackingsAction.php @@ -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. " . diff --git a/src/main/php/com/fwdekker/deathnotifier/user/ChangeEmailAction.php b/src/main/php/com/fwdekker/deathnotifier/user/ChangeEmailAction.php index 168d78f..947065a 100644 --- a/src/main/php/com/fwdekker/deathnotifier/user/ChangeEmailAction.php +++ b/src/main/php/com/fwdekker/deathnotifier/user/ChangeEmailAction.php @@ -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 diff --git a/src/main/php/com/fwdekker/deathnotifier/user/ChangePasswordAction.php b/src/main/php/com/fwdekker/deathnotifier/user/ChangePasswordAction.php index 8ac6be9..e4ba8ac 100644 --- a/src/main/php/com/fwdekker/deathnotifier/user/ChangePasswordAction.php +++ b/src/main/php/com/fwdekker/deathnotifier/user/ChangePasswordAction.php @@ -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." . diff --git a/src/main/php/com/fwdekker/deathnotifier/user/RegisterAction.php b/src/main/php/com/fwdekker/deathnotifier/user/RegisterAction.php index 774d79a..723f720 100644 --- a/src/main/php/com/fwdekker/deathnotifier/user/RegisterAction.php +++ b/src/main/php/com/fwdekker/deathnotifier/user/RegisterAction.php @@ -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 diff --git a/src/main/php/com/fwdekker/deathnotifier/user/ResendVerifyEmailAction.php b/src/main/php/com/fwdekker/deathnotifier/user/ResendVerifyEmailAction.php index 41a0cc4..36da3b3 100644 --- a/src/main/php/com/fwdekker/deathnotifier/user/ResendVerifyEmailAction.php +++ b/src/main/php/com/fwdekker/deathnotifier/user/ResendVerifyEmailAction.php @@ -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 diff --git a/src/main/php/com/fwdekker/deathnotifier/user/ResetPasswordAction.php b/src/main/php/com/fwdekker/deathnotifier/user/ResetPasswordAction.php index 17f60a1..c776adb 100644 --- a/src/main/php/com/fwdekker/deathnotifier/user/ResetPasswordAction.php +++ b/src/main/php/com/fwdekker/deathnotifier/user/ResetPasswordAction.php @@ -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." . diff --git a/src/main/php/com/fwdekker/deathnotifier/user/SendPasswordResetAction.php b/src/main/php/com/fwdekker/deathnotifier/user/SendPasswordResetAction.php index 98f51cc..4cea10a 100644 --- a/src/main/php/com/fwdekker/deathnotifier/user/SendPasswordResetAction.php +++ b/src/main/php/com/fwdekker/deathnotifier/user/SendPasswordResetAction.php @@ -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"; diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/EqualsCliPasswordRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/EqualsCliPasswordRule.php new file mode 100644 index 0000000..ee5822c --- /dev/null +++ b/src/main/php/com/fwdekker/deathnotifier/validator/EqualsCliPasswordRule.php @@ -0,0 +1,48 @@ + $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); + } +} diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/EqualsCliSecretRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/EqualsCliSecretRule.php deleted file mode 100644 index 7d6c8be..0000000 --- a/src/main/php/com/fwdekker/deathnotifier/validator/EqualsCliSecretRule.php +++ /dev/null @@ -1,41 +0,0 @@ - $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); - } -} diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/HasStringLengthRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/HasStringLengthRule.php index 1f0580e..c2803a4 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/HasStringLengthRule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/HasStringLengthRule.php @@ -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); } } diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/IsBooleanRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/IsBooleanRule.php index ed55095..2ab701d 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/IsBooleanRule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/IsBooleanRule.php @@ -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); } } diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/IsEmailRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/IsEmailRule.php index 2c73613..c6fc590 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/IsEmailRule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/IsEmailRule.php @@ -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 $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); } } diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/IsEqualToRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/IsEqualToRule.php deleted file mode 100644 index 0455793..0000000 --- a/src/main/php/com/fwdekker/deathnotifier/validator/IsEqualToRule.php +++ /dev/null @@ -1,48 +0,0 @@ -expected = $expected; - } - - - /** - * Validates that the input has the specified value. - * - * @param array $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 - ); - } -} diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/IsNotBlankRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/IsNotBlankRule.php index b38a655..75c75cb 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/IsNotBlankRule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/IsNotBlankRule.php @@ -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); } } diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/IsNotSetRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/IsNotSetRule.php index c4a51ab..dba5d3d 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/IsNotSetRule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/IsNotSetRule.php @@ -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. * diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/IsSetRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/IsSetRule.php index 862a55d..3c27bc9 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/IsSetRule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/IsSetRule.php @@ -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. * diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/IsStringRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/IsStringRule.php index 4c83960..765c6a3 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/IsStringRule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/IsStringRule.php @@ -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); } } diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/IsValidCsrfTokenRule.php b/src/main/php/com/fwdekker/deathnotifier/validator/IsValidCsrfTokenRule.php index a4b9fb8..9a0dab1 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/IsValidCsrfTokenRule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/IsValidCsrfTokenRule.php @@ -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 $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); } } diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/Rule.php b/src/main/php/com/fwdekker/deathnotifier/validator/Rule.php index 50cb661..dc920aa 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/Rule.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/Rule.php @@ -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 $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 $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; } diff --git a/src/main/php/com/fwdekker/deathnotifier/validator/SessionRuleSet.php b/src/main/php/com/fwdekker/deathnotifier/validator/SessionRuleSet.php index fdc8742..844aae8 100644 --- a/src/main/php/com/fwdekker/deathnotifier/validator/SessionRuleSet.php +++ b/src/main/php/com/fwdekker/deathnotifier/validator/SessionRuleSet.php @@ -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); } diff --git a/src/main/php/com/fwdekker/deathnotifier/wikipedia/Wikipedia.php b/src/main/php/com/fwdekker/deathnotifier/wikipedia/Wikipedia.php index a8cb061..11d6afc 100644 --- a/src/main/php/com/fwdekker/deathnotifier/wikipedia/Wikipedia.php +++ b/src/main/php/com/fwdekker/deathnotifier/wikipedia/Wikipedia.php @@ -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 - * @author Gabriel Sobrinho - */ -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; -} diff --git a/src/test/php/com/fwdekker/deathnotifier/ConfigTest.php b/src/test/php/com/fwdekker/deathnotifier/ConfigTest.php new file mode 100644 index 0000000..bf6f089 --- /dev/null +++ b/src/test/php/com/fwdekker/deathnotifier/ConfigTest.php @@ -0,0 +1,103 @@ + "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")); + } +} diff --git a/src/test/php/com/fwdekker/deathnotifier/DatabaseTestCase.php b/src/test/php/com/fwdekker/deathnotifier/DatabaseTestCase.php index 52a38f7..d16b49d 100644 --- a/src/test/php/com/fwdekker/deathnotifier/DatabaseTestCase.php +++ b/src/test/php/com/fwdekker/deathnotifier/DatabaseTestCase.php @@ -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; /** diff --git a/src/test/php/com/fwdekker/deathnotifier/validator/EqualsCliPasswordRuleTest.php b/src/test/php/com/fwdekker/deathnotifier/validator/EqualsCliPasswordRuleTest.php new file mode 100644 index 0000000..9def699 --- /dev/null +++ b/src/test/php/com/fwdekker/deathnotifier/validator/EqualsCliPasswordRuleTest.php @@ -0,0 +1,68 @@ +|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|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], + ]; + } +} diff --git a/src/test/php/com/fwdekker/deathnotifier/validator/IsEmailRuleTest.php b/src/test/php/com/fwdekker/deathnotifier/validator/IsEmailRuleTest.php index 0816690..b79e9f1 100644 --- a/src/test/php/com/fwdekker/deathnotifier/validator/IsEmailRuleTest.php +++ b/src/test/php/com/fwdekker/deathnotifier/validator/IsEmailRuleTest.php @@ -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], + ]; } } diff --git a/src/test/php/com/fwdekker/deathnotifier/validator/IsNotBlankRuleTest.php b/src/test/php/com/fwdekker/deathnotifier/validator/IsNotBlankRuleTest.php deleted file mode 100644 index ab8a718..0000000 --- a/src/test/php/com/fwdekker/deathnotifier/validator/IsNotBlankRuleTest.php +++ /dev/null @@ -1,55 +0,0 @@ -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"]); - } -} diff --git a/src/test/php/com/fwdekker/deathnotifier/validator/IsSetRuleTest.php b/src/test/php/com/fwdekker/deathnotifier/validator/IsSetRuleTest.php deleted file mode 100644 index 333c964..0000000 --- a/src/test/php/com/fwdekker/deathnotifier/validator/IsSetRuleTest.php +++ /dev/null @@ -1,25 +0,0 @@ -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); - } -} diff --git a/src/test/php/com/fwdekker/deathnotifier/validator/RuleTest.php b/src/test/php/com/fwdekker/deathnotifier/validator/RuleTest.php index afbb35e..0cf600f 100644 --- a/src/test/php/com/fwdekker/deathnotifier/validator/RuleTest.php +++ b/src/test/php/com/fwdekker/deathnotifier/validator/RuleTest.php @@ -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|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|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; } diff --git a/src/test/php/com/fwdekker/deathnotifier/validator/ValidatorTest.php b/src/test/php/com/fwdekker/deathnotifier/validator/ValidatorTest.php deleted file mode 100644 index b78009a..0000000 --- a/src/test/php/com/fwdekker/deathnotifier/validator/ValidatorTest.php +++ /dev/null @@ -1,156 +0,0 @@ - "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); - } -}