Add unit tests for server-side input validation
This commit is contained in:
parent
d20147101c
commit
4bca46fb97
16
Gruntfile.js
16
Gruntfile.js
|
@ -57,12 +57,16 @@ module.exports = grunt => {
|
|||
},
|
||||
run: {
|
||||
composer_dev: {
|
||||
cmd: "composer.phar",
|
||||
args: ["install"]
|
||||
exec: "composer.phar install"
|
||||
},
|
||||
composer: {
|
||||
cmd: "composer.phar",
|
||||
args: ["install", "--no-dev"]
|
||||
exec: "composer.phar install --no-dev"
|
||||
},
|
||||
phpunit: {
|
||||
exec: "vendor/bin/phpunit --testdox src/test"
|
||||
},
|
||||
stan: {
|
||||
exec: "vendor/bin/phpstan analyse -l 8 src/main src/test"
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
@ -151,5 +155,9 @@ module.exports = grunt => {
|
|||
"replace:deploy",
|
||||
]);
|
||||
|
||||
grunt.registerTask("analyze", ["run:stan"]);
|
||||
grunt.registerTask("check", ["dev", "run:stan", "run:phpunit"]);
|
||||
grunt.registerTask("test", ["dev", "run:phpunit"]);
|
||||
|
||||
grunt.registerTask("default", ["dev"]);
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ This tool regularly checks if people are still alive according to Wikipedia, and
|
|||
### Requirements
|
||||
* PHP 8.1+ (i.e. `apt install php php-cgi`)
|
||||
* [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 SQLite 3](https://www.php.net/manual/en/book.sqlite3.php) (i.e. `apt install php-sqlite3`)
|
||||
* [composer](https://getcomposer.org/)
|
||||
* [npm](https://www.npmjs.com/)
|
||||
|
@ -41,8 +42,12 @@ $> npm install
|
|||
|
||||
### Static analysis
|
||||
```shell script
|
||||
# PHP static analysis
|
||||
$> npm run stan
|
||||
# Run static analysis
|
||||
$> npm run analyze
|
||||
# Run tests
|
||||
$> npm run test
|
||||
# Run static analysis and tests
|
||||
$> npm run check
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -23,9 +23,14 @@
|
|||
"phpmailer/phpmailer": "^6.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.9"
|
||||
"phpstan/phpstan": "^1.9",
|
||||
"phpunit/phpunit": "^9.5"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/main/php/",
|
||||
"src/test/php/"
|
||||
],
|
||||
"psr-4": {
|
||||
"php\\": "php/"
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "death-notifier",
|
||||
"version": "0.14.7", "_comment_version": "Also update version in `composer.json`!",
|
||||
"version": "0.14.8", "_comment_version": "Also update version in `composer.json`!",
|
||||
"description": "Get notified when a famous person dies.",
|
||||
"author": "Florine W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
@ -10,11 +10,13 @@
|
|||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "grunt analyze",
|
||||
"check": "grunt check",
|
||||
"clean": "grunt clean",
|
||||
"dev": "grunt dev",
|
||||
"dev:server": "grunt dev:server",
|
||||
"deploy": "grunt deploy",
|
||||
"stan": "vendor/bin/phpstan analyse -l 8 src/main"
|
||||
"test": "grunt test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"grunt": "^1.5.3",
|
||||
|
|
|
@ -37,13 +37,12 @@ a.red-link {
|
|||
/*noinspection CssUnresolvedCustomProperty*/
|
||||
margin-bottom: var(--typography-spacing-vertical);
|
||||
|
||||
border-bottom: .1rem solid #e1e1e1;
|
||||
overflow-y: scroll;
|
||||
|
||||
/* Shows scrolling shadows. Adapted from https://css-tricks.com/books/greatest-css-tricks/scroll-shadows/ */
|
||||
/*noinspection CssUnresolvedCustomProperty*/
|
||||
background: linear-gradient(white 30%, rgba(255, 255, 255, 0)) no-repeat local center calc(var(--spacing) + 1.6em),
|
||||
linear-gradient(rgba(255, 255, 255, 0), white 70%) no-repeat local center bottom,
|
||||
background: linear-gradient(var(--card-background-color) 30%, rgba(255, 255, 255, 0)) no-repeat local center calc(var(--spacing) + 1.6em),
|
||||
linear-gradient(rgba(255, 255, 255, 0), var(--card-background-color) 70%) no-repeat local center bottom,
|
||||
radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) no-repeat scroll center calc(var(--spacing) + 1.6em),
|
||||
radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) no-repeat scroll center bottom;
|
||||
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
|
||||
|
@ -56,7 +55,8 @@ a.red-link {
|
|||
#trackings thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: white;
|
||||
/*noinspection CssUnresolvedCustomProperty*/
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
|
||||
#trackings td {
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
<meta name="theme-color" content="#0033cc" />
|
||||
|
||||
<meta name="fwd:nav:target" content="#nav" />
|
||||
<meta name="fwd:nav:highlight-path" content="/Tools/Death-Notifier/" />
|
||||
<!-- Remove experimental after release -->
|
||||
<meta name="fwd:nav:highlight-path" content="/Tools/Experimental/Death Notifier/" />
|
||||
<meta name="fwd:footer:target" content="#footer" />
|
||||
<meta name="fwd:footer:vcs-url" content="https://git.fwdekker.com/tools/death-notifier/" />
|
||||
<meta name="fwd:footer:version" content="v%%VERSION_NUMBER%%" />
|
||||
|
@ -108,7 +109,6 @@
|
|||
</hgroup>
|
||||
</header>
|
||||
<div class="article-contents">
|
||||
<!-- TODO: Receive requirements from server(?) (combine this with client-side validation) -->
|
||||
<form id="register-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="register-form">
|
||||
<output></output>
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
use php\IsEmailRule;
|
||||
use php\Rule;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for `IsEmailRule`.
|
||||
*/
|
||||
final class IsEmailRuleTest extends RuleTest
|
||||
{
|
||||
function get_rule(?string $override = null): Rule
|
||||
{
|
||||
return new IsEmailRule($override);
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
$this->assertNull($is_valid);
|
||||
}
|
||||
|
||||
public function test_returns_response_message_if_email_is_invalid(): void
|
||||
{
|
||||
$rule = new IsEmailRule();
|
||||
|
||||
$is_valid = $rule->check(["email" => "exampleexample.com"], "email");
|
||||
|
||||
$this->assertNotNull($is_valid);
|
||||
$this->assertEquals("Enter a valid email address.", $is_valid->payload["message"]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
use php\IsNotBlankRule;
|
||||
use php\Rule;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for `IsNotBlankRule`.
|
||||
*/
|
||||
final 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");
|
||||
|
||||
$this->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");
|
||||
|
||||
$this->assertNotNull($is_valid);
|
||||
$this->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");
|
||||
|
||||
$this->assertNotNull($is_valid);
|
||||
$this->assertEquals("Use at least one character.", $is_valid->payload["message"]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
use php\IsSetRule;
|
||||
use php\Rule;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for `IsSetRule`.
|
||||
*/
|
||||
final 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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
use php\LengthRule;
|
||||
use php\Rule;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for `LengthRule`.
|
||||
*/
|
||||
final class LengthRuleTest extends RuleTest
|
||||
{
|
||||
function get_rule(?string $override = null): Rule
|
||||
{
|
||||
return new LengthRule(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 LengthRule(1, 3);
|
||||
|
||||
$is_valid = $rule->check(["input" => "a"], "input");
|
||||
|
||||
$this->assertNull($is_valid);
|
||||
}
|
||||
|
||||
public function test_returns_null_if_input_is_exactly_maximum_length(): void
|
||||
{
|
||||
$rule = new LengthRule(1, 3);
|
||||
|
||||
$is_valid = $rule->check(["input" => "123"], "input");
|
||||
|
||||
$this->assertNull($is_valid);
|
||||
}
|
||||
|
||||
public function test_returns_null_if_input_is_strictly_inside_range(): void
|
||||
{
|
||||
$rule = new LengthRule(1, 3);
|
||||
|
||||
$is_valid = $rule->check(["input" => "12"], "input");
|
||||
|
||||
$this->assertNull($is_valid);
|
||||
}
|
||||
|
||||
public function test_returns_not_null_if_input_is_strictly_below_minimum(): void
|
||||
{
|
||||
$rule = new LengthRule(1, 3);
|
||||
|
||||
$is_valid = $rule->check(["input" => ""], "input");
|
||||
|
||||
$this->assertNotNull($is_valid);
|
||||
}
|
||||
|
||||
public function test_returns_not_null_if_input_is_strictly_above_maximum(): void
|
||||
{
|
||||
$rule = new LengthRule(1, 3);
|
||||
|
||||
$is_valid = $rule->check(["input" => "1234"], "input");
|
||||
|
||||
$this->assertNotNull($is_valid);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
use Monolog\Test\TestCase;
|
||||
use php\Rule;
|
||||
|
||||
|
||||
/**
|
||||
* Inheritable unit tests for extensions of `Rule`.
|
||||
*/
|
||||
abstract class RuleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Constructs a new `Rule` of the class under test, using `override`.
|
||||
*
|
||||
* 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`
|
||||
*/
|
||||
abstract function get_rule(?string $override = null): Rule;
|
||||
|
||||
/**
|
||||
* @return string|null a valid input to give to `get_rule`
|
||||
*/
|
||||
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");
|
||||
|
||||
$this->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");
|
||||
|
||||
$this->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");
|
||||
|
||||
$this->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");
|
||||
|
||||
$this->assertNotNull($is_valid);
|
||||
$this->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");
|
||||
|
||||
$this->assertNotNull($is_valid);
|
||||
$this->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"
|
||||
);
|
||||
|
||||
$this->assertNotNull($is_valid);
|
||||
$this->assertEquals("invalid", $is_valid->payload["target"]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
<?php
|
||||
|
||||
use Monolog\Test\TestCase;
|
||||
use php\IsEmailRule;
|
||||
use php\IsNotBlankRule;
|
||||
use php\IsSetRule;
|
||||
use php\Validator;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for `Validator`.
|
||||
*/
|
||||
final 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);
|
||||
|
||||
$this->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);
|
||||
|
||||
$this->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);
|
||||
|
||||
$this->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);
|
||||
|
||||
$this->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);
|
||||
|
||||
$this->assertNotNull($is_valid);
|
||||
$this->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);
|
||||
|
||||
$this->assertNotNull($is_valid);
|
||||
$this->assertEquals("among", $is_valid->payload["target"]);
|
||||
$this->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);
|
||||
|
||||
$this->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);
|
||||
|
||||
$this->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);
|
||||
|
||||
$this->assertNotNull($is_valid);
|
||||
}
|
||||
|
||||
function test_validate_logged_out_returns_null_if_uuid_is_not_set(): void
|
||||
{
|
||||
$session = [];
|
||||
|
||||
$is_valid = Validator::validate_logged_out($session);
|
||||
|
||||
$this->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);
|
||||
|
||||
$this->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);
|
||||
|
||||
$this->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");
|
||||
|
||||
$this->assertNotNull($is_valid);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue