Compare commits

...

6 Commits
v1.0.1 ... main

Author SHA1 Message Date
Florine W. Dekker d7c2658ba6
Resolve error in default ini 2023-09-04 15:04:08 +02:00
Florine W. Dekker 2daa74b319
Make user agent customisable
And fix some instructions in the README, and put config settings between quotes where applicable.
2023-09-04 14:54:45 +02:00
Florine W. Dekker 6a1be5ac2f
Use Argon2 instead of bcrypt
Passwords currently stored under bcrypt are not automatically converted. Argon2 is only used for new passwords from this moment on.
2023-08-30 22:35:35 +02:00
Florine W. Dekker 7b86673590
Update dependencies and ensure Hitler is dead
Hitler's category was changed from "1945 deaths" to "1945 suicides", resulting in Death Notifier not detecting that Hitler was dead (or even a person). Death Notifier now recognises the "<year> suicides" category.
2023-08-30 22:31:05 +02:00
Florine W. Dekker e024bc4cf8
Update a few strings for clarity 2023-08-29 21:00:09 +02:00
Florine W. Dekker 80835d6a08
Create separate log for database events
Breaking: The option `[logger.file]` has been renamed to `[logger.filename]` for consistency.
2023-03-20 13:55:31 +01:00
26 changed files with 169 additions and 99 deletions

View File

@ -1,7 +1,7 @@
const path = require("path");
module.exports = grunt => {
const testTarget = grunt.option("test-target") || ".";
const testFilter = grunt.option("test-filter") || "'' .";
grunt.initConfig({
pkg: grunt.file.readJSON("package.json"),
@ -71,7 +71,7 @@ module.exports = grunt => {
exec: "composer.phar install --no-dev"
},
phpunit: {
exec: `cd dist/ && chmod +x .vendor/bin/phpunit && .vendor/bin/phpunit --testdox ${testTarget}`
exec: `cd dist/ && chmod +x .vendor/bin/phpunit && .vendor/bin/phpunit --filter ${testFilter}`
},
stan: {
exec: "vendor/bin/phpstan analyse -l 8 src/main src/test"

View File

@ -9,15 +9,16 @@ This tool regularly checks if people are still alive according to Wikipedia, and
## Development
### Requirements
* PHP 8.1+ (i.e. `apt install php php-cgi`)
* PHP 8.1+ (i.e. `apt install php php-cgi`) (compiled with Argon2 support)
* [PHP cURL](https://www.php.net/manual/en/book.curl.php) (i.e. `apt install php-curl`)
* [PHP DOM](https://www.php.net/manual/en/book.dom.php) (i.e. `apt install php-dom`)
* [PHP mbstring](https://www.php.net/manual/en/book.mbstring.php) (i.e. `apt install php-mbstring`)
* [PHP SQLite 3](https://www.php.net/manual/en/book.sqlite3.php) (i.e. `apt install php-sqlite3`)
* [composer](https://getcomposer.org/) (make sure `composer.phar` is on your path)
* [npm](https://www.npmjs.com/)
### Setting up
Install the latest dependencies.
Install the dependencies.
Run this after cloning the repo, and each time after pulling new commits.
```shell
composer.phar install
@ -45,7 +46,10 @@ composer.phar update
npm install
```
### Static analysis
### Static analysis and tests
Note that PHPUnit suppresses output from `print`.
Instead, you can use `fwrite(STDERR, print_r("my message", TRUE));`.
* Run static analysis
```shell
npm run analyze
@ -54,10 +58,24 @@ npm install
```shell
npm run test
```
* Run all tests in package
* Run only select tests
```shell
npm run test -- --test-target=com/fwdekker/deathnotifier/wikipedia/
npm run test -- --test-filter="test-name file-name"
```
Note that a `test-name` of `''` and a `file-name` of `.` matches all tests.
* Run all tests in package
```shell
npm run test -- --test-filter="'' com/fwdekker/deathnotifier/wikipedia/"
```
* Run all tests in class
```shell
npm run test -- --test-filter="'' com/fwdekker/deathnotifier/wikipedia/WikipediaTest.php"
```
* Run only specific test
```shell
npm run test -- --test-filter="test_query_detects_dead_person ."
```
* Run static analysis and tests
```shell
npm run check
@ -77,20 +95,22 @@ Inside the installation directory, create `config.ini.php` and use it to overrid
Make sure only the user that runs PHP can read/write `config.ini.php`.
### Cron jobs
You should run the `process-email-queue` and `update-all-trackings` actions regularly;
You should run the `process-email-queue` and `update-trackings` actions regularly;
recommended is every minute and every five minutes, respectively.
For example, you can add the following lines to your crontab (e.g. using `sudo -u www crontab -e`):
```
* * * * * cd /var/www/death-notifier && php /var/www/death-notifier/api.php action=process-email-queue password=secret_password
*/5 * * * * cd /var/www/death-notifier && php /var/www/death-notifier/api.php action=update-all-trackings password=secret_password
*/5 * * * * cd /var/www/death-notifier && php /var/www/death-notifier/api.php action=update-trackings password=secret_password
```
Replace `secret_password` with the password you configured in `config.ini.php`.
### Logs
It is recommended to also use a tool such as `newsyslog` to manage log rotation.
It is recommended to also use a tool such as
[`newsyslog`](https://man.freebsd.org/cgi/man.cgi?query=newsyslog.conf&sektion=5) to manage log rotation.
For example, create the file `/etc/newsyslog.conf.d/death-notifier.conf` with the following contents:
```
/var/www/death-notifier/.death-notifier.log www:www 644 7 * @T00 JpE
/var/www/death-notifier/.death-notifier.log www:www 600 7 * @T00 JE
/var/www/death-notifier/.death-notifier.db.log www:www 600 7 * $W0D23 JpE
```
### Initialize

View File

@ -1,7 +1,7 @@
{
"name": "fwdekker/death-notifier",
"description": "Get notified when a famous person dies.",
"version": "1.0.1", "_comment_version": "Also update version in `package.json`!",
"version": "1.3.0", "_comment_version": "Also update version in `package.json`!",
"type": "project",
"license": "MIT",
"homepage": "https://git.fwdekker.com/tools/death-notifier",
@ -19,12 +19,12 @@
"composer/semver": "^3.3",
"ext-curl": "*",
"ext-pdo": "*",
"monolog/monolog": "^3.3",
"monolog/monolog": "^3.4",
"phpmailer/phpmailer": "^6.8"
},
"require-dev": {
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^9.5"
"phpstan/phpstan": "^1.10.32",
"phpunit/phpunit": "^10.3.2"
},
"autoload": {
"psr-4": {

BIN
composer.lock generated

Binary file not shown.

BIN
package-lock.json generated

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "death-notifier",
"version": "1.0.1", "_comment_version": "Also update version in `composer.json`!",
"version": "1.3.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",
@ -27,10 +27,10 @@
"grunt-focus": "^1.0.0",
"grunt-run": "^0.8.1",
"grunt-text-replace": "^0.4.0",
"grunt-webpack": "^5.0.0",
"ts-loader": "^9.4.2",
"typescript": "^5.0.2",
"webpack": "^5.76.2",
"webpack-cli": "^5.0.1"
"grunt-webpack": "^6.0.0",
"ts-loader": "^9.4.4",
"typescript": "^5.2.2",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4"
}
}

View File

@ -36,7 +36,7 @@ use com\fwdekker\deathnotifier\Util;
require_once __DIR__ . "/.vendor/autoload.php";
$logger = LoggerUtil::with_name();
$logger = LoggerUtil::with_name(); // This also registers error handler
// Wrap everything in try-catch to always return *something* to user

View File

@ -1,30 +1,37 @@
;<?php exit(); ?>
[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_password = REPLACE THIS WITH A SECRET VALUE
# PHC-formatted hash of password for the CLI of `api.php`. You can create one using PHP's `password_hash`. Escaping
# dollar symbols is optional. If set to its default value, or if empty, the CLI is disabled.
cli_password = "REPLACE THIS WITH A SECRET VALUE"
[database]
# Relative path to SQLite database.
filename = .death-notifier.db
# File to store SQLite database in.
filename = ".death-notifier.db"
[logger]
# File to store logs in.
file = .death-notifier.log
# Log level. See https://seldaek.github.io/monolog/doc/01-usage.html#log-levels
# File to store general logs in.
filename = ".death-notifier.log"
# Log level for general log events. See https://seldaek.github.io/monolog/doc/01-usage.html#log-levels
level = 250
[logger_db]
# File to store database logs in.
filename = ".death-notifier.db.log"
# Log level for database log events. See https://seldaek.github.io/monolog/doc/01-usage.html#log-levels
level = 250
[mail]
# Host name of SMTP server to send mail through.
host = TODO
host = "TODO"
# Port of SMTP server to send mail through.
port = TODO
# Username to authenticate with at SMTP server.
username = TODO
username = "TODO"
# Password to authenticate with at SMTP server.
password = TODO
password = "TODO"
# Name to show to recipient.
from_name = TODO
from_name = "TODO"
[security]
# `true` if and only if insecure file permissions for config files should be tolerated.
@ -32,8 +39,13 @@ allow_config_insecure_permissions = false
[server]
# The path to the directory containing the site's main page.
base_path = https://example.com/death-notifier/
base_path = "https://example.com/death-notifier/"
# The path at which users can report bugs, or an empty string if there is no such path.
issue_path =
issue_path = ""
# The message to display at the top of all pages. A blank string hides the message.
global_message =
global_message = ""
[wikipedia]
# Contact information to include in requests to Wikipedia's API. Typically a URL and an email address separated by a
# semicolon. See also https://www.mediawiki.org/wiki/API:Etiquette.
user_agent_contact = "https://example.com/death-notifier; name@example.com"

View File

@ -409,7 +409,7 @@
<input id="delete-account-actual-email" type="hidden" name="email" />
<label for="delete-account-email">Confirm email</label>
<label for="delete-account-email">Confirm email address</label>
<input id="delete-account-email" name="email" autocomplete="off" />
<small id="delete-account-email-hint" data-hint-for="delete-account-email"></small>

View File

@ -24,7 +24,7 @@ class Database
/**
* @var Logger the logger to use for logging
*/
private readonly Logger $logger;
private readonly Logger $db_logger;
/**
* @var PDO the PDO object that connects to the database
@ -39,7 +39,7 @@ class Database
*/
public function __construct(string $filename)
{
$this->logger = LoggerUtil::with_name($this::class);
$this->db_logger = LoggerUtil::db_with_name($this::class);
$this->conn = new PDO("sqlite:$filename", options: array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
$this->conn->exec("PRAGMA foreign_keys = ON;");
@ -64,7 +64,7 @@ class Database
if ($stmt->fetch()[0] !== 0)
return;
$this->logger->notice("Database does not exist. Installing new database.");
$this->db_logger->notice("Database does not exist. Installing new database.");
// Create `meta` table
$this->conn->exec("CREATE TABLE meta(k TEXT NOT NULL UNIQUE PRIMARY KEY, v TEXT);");
@ -77,7 +77,7 @@ class Database
$user_list->install();
$tracking_list->install();
$this->logger->notice("Installation complete.");
$this->db_logger->notice("Installation complete.");
});
}
@ -96,7 +96,7 @@ class Database
if (Comparator::greaterThanOrEqualTo($db_version, Database::LATEST_VERSION))
return;
$this->logger->notice("Current db is v$db_version. Will migrate to v" . Database::LATEST_VERSION . ".");
$this->db_logger->notice("Current db is v$db_version. Will migrate to v" . Database::LATEST_VERSION . ".");
// Get current version
$stmt = $this->conn->prepare("SELECT v FROM meta WHERE k='version';");
@ -115,7 +115,7 @@ class Database
$stmt->bindValue(":version", Database::LATEST_VERSION);
$stmt->execute();
$this->logger->notice("Completed migration to v" . Database::LATEST_VERSION . ".");
$this->db_logger->notice("Completed migration to v" . Database::LATEST_VERSION . ".");
});
}
@ -127,7 +127,7 @@ class Database
*/
private function migrate_0_5_0(): void
{
$this->logger->notice("Migrating to v0.5.0.");
$this->db_logger->notice("Migrating to v0.5.0.");
$this->conn->exec("ALTER TABLE users
ADD COLUMN email_notifications_enabled INT NOT NULL DEFAULT(1);");
@ -141,7 +141,7 @@ class Database
*/
private function migrate_0_8_0(): void
{
$this->logger->notice("Migrating to v0.8.0.");
$this->db_logger->notice("Migrating to v0.8.0.");
$this->conn->exec("CREATE TABLE new_email_tasks(type TEXT NOT NULL,
recipient TEXT NOT NULL,
@ -163,7 +163,7 @@ class Database
*/
private function migrate_0_10_0(): void
{
$this->logger->notice("Migrating to v0.10.0.");
$this->db_logger->notice("Migrating to v0.10.0.");
$this->conn->exec("CREATE TABLE new_email_tasks(type TEXT NOT NULL,
recipient TEXT NOT NULL,
@ -188,7 +188,7 @@ class Database
*/
private function migrate_0_16_0(): void
{
$this->logger->notice("Migrating to v0.16.0.");
$this->db_logger->notice("Migrating to v0.16.0.");
$this->conn->exec("DROP TABLE email_tasks;");
$this->conn->exec("CREATE TABLE email_tasks(type_key TEXT NOT NULL,
@ -206,7 +206,7 @@ class Database
*/
private function migrate_0_18_0(): void
{
$this->logger->notice("Migrating to v0.18.0.");
$this->db_logger->notice("Migrating to v0.18.0.");
$this->conn->exec("CREATE TABLE new_trackings(user_uuid TEXT NOT NULL,
person_name TEXT NOT NULL,

View File

@ -15,28 +15,49 @@ use Monolog\Logger;
class LoggerUtil
{
/**
* @var Logger|null the main logger instance that other loggers are derived from, or `null` if the main logger has
* not been created yet
* @var Logger|null the logger instance for general log events not covered by other loggers
*/
private static ?Logger $main_logger = null;
private static ?Logger $general_logger = null;
/**
* @var Logger|null the logger instance for database log events
*/
private static ?Logger $db_logger = null;
/**
* Returns a logger with the given name.
* Returns a general logger with the given name.
*
* @param string $name the name of the logger to return
* @return Logger a logger with the given name
* @param string $name the name of the general logger to return
* @return Logger a general logger with the given name
*/
public static function with_name(string $name = "main"): Logger
public static function with_name(string $name = "general"): Logger
{
if (self::$main_logger === null) {
if (self::$general_logger === null) {
$config = Config::get("logger");
self::$main_logger = new Logger("main");
self::$main_logger->pushHandler(new StreamHandler($config["file"], $config["level"]));
ErrorHandler::register(self::$main_logger);
self::$general_logger = new Logger("general");
self::$general_logger->pushHandler(new StreamHandler($config["filename"], $config["level"]));
ErrorHandler::register(self::$general_logger);
}
return self::$main_logger->withName($name);
return self::$general_logger->withName($name);
}
/**
* Returns a database logger with the given name.
*
* @param string $name the name of the database logger to return
* @return Logger a database logger with the given name
*/
public static function db_with_name(string $name = "database"): Logger
{
if (self::$db_logger === null) {
$config = Config::get("logger_db");
self::$db_logger = new Logger("general");
self::$db_logger->pushHandler(new StreamHandler($config["filename"], $config["level"]));
}
return self::$db_logger->withName($name);
}
}

View File

@ -89,7 +89,7 @@ class ProcessEmailQueueAction extends Action
$mailer->SMTPKeepAlive = true;
$mailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
$mailer->Host = $config["host"];
$mailer->Port = $config["port"];
$mailer->Port = intval($config["port"]);
$mailer->Username = $config["username"];
$mailer->Password = $config["password"];
try {

View File

@ -31,6 +31,10 @@ class UpdateTrackingsAction extends Action
* @var Logger the logger to log with
*/
private readonly Logger $logger;
/**
* @var Logger the database logger to log with
*/
private readonly Logger $db_logger;
/**
* @var TrackingList the list of trackings to update
*/
@ -55,6 +59,7 @@ class UpdateTrackingsAction extends Action
public function __construct(TrackingList $tracking_list, Wikipedia $wikipedia, EmailQueue $mailer)
{
$this->logger = LoggerUtil::with_name($this::class);
$this->db_logger = LoggerUtil::db_with_name($this::class);
$this->tracking_list = $tracking_list;
$this->wikipedia = $wikipedia;
$this->mailer = $mailer;
@ -105,21 +110,20 @@ class UpdateTrackingsAction extends Action
// Send mails, log events
// TODO: Restrict number of notifications to 1 per hour (excluding "oops we're not sure" message)
// TODO: Wait for some time after person has been marked as dead before sending the email
$logger = LoggerUtil::with_name($this::class);
$emails = [];
$person_names = $new_deletions + $new_undeletions + array_keys($new_status_changes);
$trackers = $this->tracking_list->list_trackers($person_names);
foreach ($new_deletions as $new_deletion) {
$logger->notice("Deleted article $new_deletion.");
$this->db_logger->notice("Deleted article $new_deletion.");
foreach ($trackers[$new_deletion] as $user_email)
$emails[] = new NotifyArticleDeletedEmail($user_email, $new_deletion);
}
foreach ($new_undeletions as $new_undeletion) {
$logger->notice("Undeleted article $new_undeletion.");
$this->db_logger->notice("Undeleted article $new_undeletion.");
foreach ($trackers[$new_undeletion] as $user_email)
$emails[] = new NotifyArticleUndeletedEmail($user_email, $new_undeletion);
@ -127,7 +131,7 @@ class UpdateTrackingsAction extends Action
foreach ($new_status_changes as $person_name => $person_status) {
if ($person_status === PersonStatus::Alive)
$logger->notice("Person $person_name is now alive again.");
$this->db_logger->notice("Person $person_name is now alive again.");
foreach ($trackers[$person_name] as $user_email)
$emails[] = new NotifyStatusChangedEmail($user_email, $person_name, $person_status->value);
@ -179,7 +183,7 @@ class NotifyStatusChangedEmail extends Email
public function get_subject(): string
{
return "$this->name may be $this->new_status";
return "Death Notifier: $this->name may be $this->new_status";
}
public function get_body(): string
@ -191,6 +195,10 @@ class NotifyStatusChangedEmail extends Email
"For more information, read their Wikipedia article at " .
"https://en.wikipedia.org/wiki/" . rawurlencode($this->name) .
"\n\n" .
"Note that Wikipedia is not a reliable source of information and is subject to vandalism and errors. " .
"Please check if reliable news sources have reported on $this->name before spreading this message to " .
"others." .
"\n\n" .
"You are receiving this message because of the preferences in your Death Notifier account. " .
"To unsubscribe from these messages, go to the Death Notifier website, log in, and change your email " .
"preferences." .
@ -233,7 +241,7 @@ class NotifyArticleDeletedEmail extends Email
public function get_subject(): string
{
return "$this->name article has been deleted";
return "Death Notifier: $this->name article has been deleted";
}
public function get_body(): string
@ -288,7 +296,7 @@ class NotifyArticleUndeletedEmail extends Email
public function get_subject(): string
{
return "$this->name article has been re-created";
return "Death Notifier: $this->name article has been re-created";
}
public function get_body(): string

View File

@ -104,7 +104,7 @@ class UserList
VALUES (:email, :password)
RETURNING email_verification_token;");
$stmt->bindValue(":email", $email);
$stmt->bindValue(":password", password_hash($password, PASSWORD_BCRYPT));
$stmt->bindValue(":password", password_hash($password, PASSWORD_ARGON2ID));
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC)[0]["email_verification_token"];
}
@ -265,11 +265,12 @@ class UserList
public function set_password(string $uuid, string $password): void
{
$stmt = $this->database->conn->prepare("UPDATE users
SET password=:password, password_last_change=unixepoch(),
SET password=:password,
password_last_change=unixepoch(),
password_reset_token=null
WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
$stmt->bindValue(":password", password_hash($password, PASSWORD_BCRYPT));
$stmt->bindValue(":password", password_hash($password, PASSWORD_ARGON2ID));
$stmt->execute();
}

View File

@ -2,7 +2,7 @@
namespace com\fwdekker\deathnotifier\wikipedia;
use com\fwdekker\deathnotifier\LoggerUtil;
use com\fwdekker\deathnotifier\Config;
use com\fwdekker\deathnotifier\Util;
use JsonException;
@ -19,9 +19,7 @@ class Wikipedia
/**
* The user agent used to represent the death notifier to Wikipedia.
*/
private const USER_AGENT =
"death-notifier/%%VERSION_NUMBER%% " .
"(https://git.fwdekker.com/tools/death-notifier; florine@fwdekker.com)";
private const USER_AGENT_FORMAT = "death-notifier/%%VERSION_NUMBER%% (%1\$s) %2\$s";
/**
* Number of articles to query per query.
*/
@ -48,10 +46,13 @@ class Wikipedia
*/
private function api_fetch(array $params): mixed
{
$curl_version = curl_version()["version"] ?? "unknown";
$user_agent = sprintf(self::USER_AGENT_FORMAT, Config::get("wikipedia")["user_agent_contact"], $curl_version);
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_URL, self::API_URL . http_build_query($params));
curl_setopt($ch, CURLOPT_USERAGENT, self::USER_AGENT);
curl_setopt($ch, CURLOPT_USERAGENT, $user_agent);
$output = curl_exec($ch);
curl_close($ch);
@ -262,7 +263,7 @@ class Wikipedia
return null;
$category_titles = array_column($article["categories"], "title");
$dead_regex = "/^Category:([0-9]{1,4}s? (BC |AD )?deaths|Year of death (missing|unknown))$/";
$dead_regex = "/^Category:([0-9]{1,4}s? (BC |AD )?(deaths|suicides)|Year of death (missing|unknown))$/";
if (!empty(array_filter($category_titles, fn($it) => preg_match($dead_regex, $it))))
return PersonStatus::Dead;

View File

@ -1,6 +1,6 @@
<?php
namespace com\fwdekker\deathnotifier\validation;
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\Config;
use PHPUnit\Framework\TestCase;

View File

@ -44,9 +44,9 @@ class EqualsCliPasswordRuleTest extends TestCase
* Returns the test cases.
*
* @return array<string, array{string|null, string|null, class-string<Throwable>|null, string|null}> the test cases
* @see RuleTest::test_check()
* @see RuleTestTemplate::test_check()
*/
public function check_provider(): array
public static function check_provider(): array
{
$hash = "\$2y\$04\$fwXTw7Rjzw0EpU094u4agOBaBNqtCHGc4TMoxfbPrxuqO5tpYyRka"; # Hash of "password"

View File

@ -8,9 +8,9 @@ use InvalidArgumentException;
/**
* Unit tests for {@see EqualsRule}.
*/
class EqualsRuleTest extends RuleTest
class EqualsRuleTest extends RuleTestTemplate
{
public function check_provider(): array
public static function check_provider(): array
{
$type = InvalidTypeException::class;
$value = InvalidValueException::class;

View File

@ -8,9 +8,9 @@ use InvalidArgumentException;
/**
* Unit tests for {@see HasStringLengthRule}.
*/
class HasStringLengthRuleTest extends RuleTest
class HasStringLengthRuleTest extends RuleTestTemplate
{
public function check_provider(): array
public static function check_provider(): array
{
$type = InvalidTypeException::class;
$value = InvalidValueException::class;

View File

@ -6,9 +6,9 @@ namespace com\fwdekker\deathnotifier\validation;
/**
* Unit tests for {@see IsBooleanRule}.
*/
class IsBooleanRuleTest extends RuleTest
class IsBooleanRuleTest extends RuleTestTemplate
{
public function check_provider(): array
public static function check_provider(): array
{
$type = InvalidTypeException::class;

View File

@ -6,9 +6,9 @@ namespace com\fwdekker\deathnotifier\validation;
/**
* Unit tests for {@see IsEmailRule}.
*/
class IsEmailRuleTest extends RuleTest
class IsEmailRuleTest extends RuleTestTemplate
{
public function check_provider(): array
public static function check_provider(): array
{
$type = InvalidTypeException::class;
$value = InvalidValueException::class;

View File

@ -6,9 +6,9 @@ namespace com\fwdekker\deathnotifier\validation;
/**
* Unit tests for {@see IsNotBlankRule}.
*/
class IsNotBlankRuleTest extends RuleTest
class IsNotBlankRuleTest extends RuleTestTemplate
{
public function check_provider(): array
public static function check_provider(): array
{
$type = InvalidTypeException::class;
$value = InvalidValueException::class;

View File

@ -6,9 +6,9 @@ namespace com\fwdekker\deathnotifier\validation;
/**
* Unit tests for {@see IsStringRule}.
*/
class IsStringRuleTest extends RuleTest
class IsStringRuleTest extends RuleTestTemplate
{
public function check_provider(): array
public static function check_provider(): array
{
$type = InvalidTypeException::class;

View File

@ -6,9 +6,9 @@ namespace com\fwdekker\deathnotifier\validation;
/**
* Unit tests for {@see IsValidCsrfTokenRule}.
*/
class IsValidCsrfTokenRuleTest extends RuleTest
class IsValidCsrfTokenRuleTest extends RuleTestTemplate
{
public function check_provider(): array
public static function check_provider(): array
{
$type = InvalidTypeException::class;

View File

@ -9,7 +9,7 @@ use Throwable;
/**
* Unit tests for {@see Rule} implementations.
*/
abstract class RuleTest extends TestCase
abstract class RuleTestTemplate extends TestCase
{
/**
* Tests the output of {@see Rule::check()}.
@ -40,7 +40,7 @@ abstract class RuleTest extends TestCase
* Returns the test cases.
*
* @return array<string, array{Rule, mixed|null, class-string<Throwable>|null, string|null}> the test cases
* @see RuleTest::test_check()
* @see RuleTestTemplate::test_check()
*/
abstract public function check_provider(): array;
abstract public static function check_provider(): array;
}

View File

@ -135,6 +135,13 @@ class WikipediaTest extends TestCase
}
public function test_query_detects_dead_person(): void
{
$output = $this->wikipedia->query_people_info(["Sophie (musician)"], resolve_moves: false);
self::assertEquals(PersonStatus::Dead, $output->results["Sophie (musician)"]["status"]);
}
public function test_query_detects_suicide_person(): void
{
$output = $this->wikipedia->query_people_info(["Adolf Hitler"], resolve_moves: false);
@ -150,8 +157,8 @@ class WikipediaTest extends TestCase
public function test_query_detects_possibly_living_person(): void
{
$output = $this->wikipedia->query_people_info(["Judge Edward Aaron"], resolve_moves: false);
$output = $this->wikipedia->query_people_info(["Angela Anderes"], resolve_moves: false);
self::assertEquals(PersonStatus::PossiblyAlive, $output->results["Judge Edward Aaron"]["status"]);
self::assertEquals(PersonStatus::PossiblyAlive, $output->results["Angela Anderes"]["status"]);
}
}