Browse Source

Merge master and fix typehints.

mscherer 6 years ago
parent
commit
f510280815

+ 8 - 2
.travis.yml

@@ -1,7 +1,5 @@
 language: php
 
-sudo: false
-
 php:
   - 7.2
   - 7.3
@@ -12,6 +10,10 @@ env:
   global:
     - DEFAULT=1
 
+services:
+  - postgresql
+  - mysql
+
 matrix:
   fast_finish: true
 
@@ -56,5 +58,9 @@ script:
 after_success:
   - if [[ $CODECOVERAGE == 1 ]]; then bash <(curl -s https://codecov.io/bash); fi
 
+cache:
+  directories:
+    - $HOME/.composer/cache
+
 notifications:
   email: false

+ 2 - 1
docs/Helper/Format.md

@@ -2,7 +2,7 @@
 
 A CakePHP helper to handle some common format topics.
 
-### Setup
+## Setup
 Include helper in your AppView class as
 ```php
 $this->addHelper('Tools.Format', [
@@ -12,6 +12,7 @@ $this->addHelper('Tools.Format', [
 
 You can store default configs also in Configure key `'Format'`.
 
+## Usage
 
 ### icon()
 Display font icons using the default namespace or an already prefixed one.

+ 68 - 0
docs/Helper/Progress.md

@@ -0,0 +1,68 @@
+# Progress Helper
+
+A CakePHP helper to handle basic progress calculation and output.
+By default it uses unicode chars to work completely text-based.
+
+The main advantage of the progress helper over default round() calculation is that it only fully displays
+0 and 100 percent borders (including the char icon representation) if truly fully that min/max value.
+So for `0.9999` as well as `0.0001` etc it will not yet display the completely full or empty bar.
+If you want that, you need to pre-round before passing it in.
+
+
+## Setup
+Include helper in your AppView class as
+```php
+$this->addHelper('Tools.Progress', [
+    ...
+]);
+```
+
+You can store default configs also in Configure key `'Progress'`.
+Mainly empty/full chars can be configured this way.
+
+## Usage
+
+### progressBar()
+Display a text-based progress bar with the progress in percentage as title.
+```php
+echo $this->Progress->progressBar(
+    $percentage // Value 0...1
+    $length, // Char length >= 3 
+    $attributes
+);
+```
+
+### draw()
+Display a text-based progress bar as raw bar.
+```php
+echo $this->Progress->draw(
+    $percentage // Value 0...1
+    $length // Char length >= 3 
+);
+```
+This can be used if you want to customize the usage.
+
+### calculatePercentage()
+
+This method is responsible for the main percentage calculation.
+It can be also used standalone.
+```php 
+$percentage = $this->Progress->calculatePercentage($total, $is);
+echo $this->Number->toPercentage($percentage, 0, ['multiply' => true]);
+```
+
+### roundPercentage()
+
+This method is responsible for the above min/max handling.
+It can be also used standalone.
+```php 
+$percentage = $this->Progress->roundPercentage($value);
+echo $this->Number->toPercentage($percentage, 0, ['multiply' => true]);
+```
+For value `0.49` it outputs: `49%`, for value `0.0001` it outputs `1%`.
+And of course `0.99999` should still be "only" `99%`.
+
+## Tips
+
+Consider using CSS `white-space: nowrap` for the span tag if wrapping could occur based on smaller display sizes.
+Wrapping would render such a text-based progress bar a bit hard to read.

+ 1 - 0
docs/README.md

@@ -48,6 +48,7 @@ Helpers:
 * [Form](Helper/Form.md)
 * [Common](Helper/Common.md)
 * [Format](Helper/Format.md)
+* [Progress](Helper/Progress.md)
 * [Tree](Helper/Tree.md)
 * [Typography](Helper/Typography.md)
 

+ 1 - 1
docs/TestSuite/Testing.md

@@ -77,7 +77,7 @@ class FooBarShellTest extends TestCase {
     /**
      * @return void
      */
-    public function setUp() {
+    public function setUp(): void {
         parent::setUp();
 
         $this->out = new ConsoleOutput();

+ 17 - 1
src/Controller/ShuntRequestController.php

@@ -23,6 +23,22 @@ use RuntimeException;
 class ShuntRequestController extends AppController {
 
 	/**
+	 * @var string|false
+	 */
+	public $modelClass = false;
+
+	/**
+	 * @return void
+	 */
+	public function initialize(): void {
+		parent::initialize();
+
+		if (!isset($this->Flash)) {
+			$this->loadComponent('Flash');
+		}
+	}
+
+	/**
 	 * Switch language as post link.
 	 *
 	 * @param string|null $language
@@ -36,7 +52,7 @@ class ShuntRequestController extends AppController {
 		if (!$language) {
 			$language = Configure::read('Config.defaultLanguage');
 		}
-		if (!$language) {
+		if (!$language && $allowedLanguages) {
 			$keys = array_keys($allowedLanguages);
 			$language = $allowedLanguages[array_shift($keys)];
 		}

+ 5 - 5
src/Database/Type/ArrayType.php

@@ -2,7 +2,7 @@
 
 namespace Tools\Database\Type;
 
-use Cake\Database\Driver;
+use Cake\Database\DriverInterface;
 use Cake\Database\Type\BaseType;
 
 /**
@@ -22,10 +22,10 @@ class ArrayType extends BaseType {
 	 * Casts given value from a PHP type to one acceptable by a database.
 	 *
 	 * @param mixed $value Value to be converted to a database equivalent.
-	 * @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted.
+	 * @param \Cake\Database\DriverInterface $driver Object from which database preferences and configuration will be extracted.
 	 * @return mixed Given PHP type casted to one acceptable by a database.
 	 */
-	public function toDatabase($value, Driver $driver) {
+	public function toDatabase($value, DriverInterface $driver) {
 		return $value;
 	}
 
@@ -33,10 +33,10 @@ class ArrayType extends BaseType {
 	 * Casts given value from a database type to a PHP equivalent.
 	 *
 	 * @param mixed $value Value to be converted to PHP equivalent
-	 * @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted
+	 * @param \Cake\Database\DriverInterface $driver Object from which database preferences and configuration will be extracted
 	 * @return mixed Given value casted from a database to a PHP equivalent.
 	 */
-	public function toPHP($value, Driver $driver) {
+	public function toPHP($value, DriverInterface $driver) {
 		return $value;
 	}
 

+ 6 - 2
src/Error/ErrorHandler.php

@@ -4,6 +4,8 @@ namespace Tools\Error;
 
 use Cake\Error\ErrorHandler as CoreErrorHandler;
 use Cake\Log\Log;
+use Cake\Routing\Router;
+use Psr\Http\Message\ServerRequestInterface;
 use Throwable;
 
 /**
@@ -37,15 +39,17 @@ class ErrorHandler extends CoreErrorHandler {
 	 * Handles exception logging
 	 *
 	 * @param \Throwable $exception Exception instance.
+	 * @param \Psr\Http\Message\ServerRequestInterface|null $request
 	 * @return bool
 	 */
-	protected function _logException(Throwable $exception): bool {
+	protected function _logException(Throwable $exception, ?ServerRequestInterface $request = null): bool {
 		if ($this->is404($exception)) {
 			$level = LOG_ERR;
 			Log::write($level, $this->_getMessage($exception), ['404']);
 			return false;
 		}
-		return parent::_logException($exception);
+
+		return parent::_logException($exception, $request ?? Router::getRequest());
 	}
 
 }

+ 2 - 2
src/Error/Middleware/ErrorHandlerMiddleware.php

@@ -51,14 +51,14 @@ class ErrorHandlerMiddleware extends CoreErrorHandlerMiddleware {
 	 * @param \Exception $exception The exception to log a message for.
 	 * @return void
 	 */
-	protected function logException(ServerRequestInterface $request, Throwable $exception): void {
+	protected function _logException(ServerRequestInterface $request, Throwable $exception): void {
 		if ($this->is404($exception, $request)) {
 			$level = LOG_ERR;
 			Log::write($level, $this->getMessage($request, $exception), ['404']);
 			return;
 		}
 
-		parent::logException($request, $exception);
+		parent::_logException($request, $exception);
 	}
 
 }

+ 3 - 2
src/Mailer/Email.php

@@ -5,6 +5,7 @@ namespace Tools\Mailer;
 use Cake\Core\Configure;
 use Cake\Log\LogTrait;
 use Cake\Mailer\Email as CakeEmail;
+use Cake\Mailer\Message;
 use InvalidArgumentException;
 use Psr\Log\LogLevel;
 use Tools\Utility\Mime;
@@ -53,7 +54,7 @@ class Email extends CakeEmail {
 	/**
 	 * Sets wrap length.
 	 *
-	 * @param int $length Must not be more than CakeEmail::LINE_LENGTH_MUST
+	 * @param int $length Must not be more than Message::LINE_LENGTH_MUST
 	 * @return $this
 	 */
 	public function setWrapLength($length) {
@@ -79,7 +80,7 @@ class Email extends CakeEmail {
 	 * @param int $wrapLength
 	 * @return array Wrapped message
 	 */
-	protected function _wrap($message, $wrapLength = CakeEmail::LINE_LENGTH_MUST) {
+	protected function _wrap($message, $wrapLength = Message::LINE_LENGTH_MUST) {
 		if ($this->_wrapLength !== null) {
 			$wrapLength = $this->_wrapLength;
 		}

+ 1 - 1
src/Model/Entity/EnumTrait.php

@@ -9,7 +9,7 @@ trait EnumTrait {
 	 * Now also supports reordering/filtering
 	 *
 	 * @link https://www.dereuromark.de/2010/06/24/static-enums-or-semihardcoded-attributes/
-	 * @param string|array|null $value Integer or array of keys or NULL for complete array result
+	 * @param int|string|array|null $value Integer or array of keys or NULL for complete array result
 	 * @param array $options Options
 	 * @param string|null $default Default value
 	 * @return string|array

+ 3 - 1
src/Model/Table/Table.php

@@ -96,7 +96,9 @@ class Table extends ShimTable {
 	 * @return void
 	 */
 	public function truncate() {
-		$sql = $this->getSchema()->truncateSql($this->_connection);
+		/** @var \Cake\Database\Schema\SqlGeneratorInterface $schema */
+		$schema = $this->getSchema();
+		$sql = $schema->truncateSql($this->_connection);
 		foreach ($sql as $snippet) {
 			$this->_connection->execute($snippet);
 		}

+ 16 - 15
src/Model/Table/TokensTable.php

@@ -126,41 +126,42 @@ class TokensTable extends Table {
 	 * @param string $key : necessary
 	 * @param mixed|null $uid : needs to be provided if this key has a user_id stored
 	 * @param bool $treatUsedAsInvalid
-	 * @return \Cake\ORM\Entity|false Content - if successfully used or if already used (used=1), FALSE else
+	 * @return \Tools\Model\Entity\Token|null Content - if successfully used or if already used (used=1), NULL otherwise.
 	 */
 	public function useKey($type, $key, $uid = null, $treatUsedAsInvalid = false) {
 		if (!$type || !$key) {
-			return false;
+			return null;
 		}
 		$options = ['conditions' => [$this->getAlias() . '.key' => $key, $this->getAlias() . '.type' => $type]];
 		if ($uid) {
 			$options['conditions'][$this->getAlias() . '.user_id'] = $uid;
 		}
-		$res = $this->find('all', $options)->first();
-		if (!$res) {
-			return false;
+		/** @var \Tools\Model\Entity\Token $tokenEntity */
+		$tokenEntity = $this->find('all', $options)->first();
+		if (!$tokenEntity) {
+			return null;
 		}
-		if ($uid && !empty($res['user_id']) && $res['user_id'] != $uid) {
+		if ($uid && !empty($tokenEntity['user_id']) && $tokenEntity['user_id'] != $uid) {
 			// return $res; # more secure to fail here if user_id is not provided, but was submitted prev.
-			return false;
+			return null;
 		}
 		// already used?
-		if (!empty($res['used'])) {
+		if (!empty($tokenEntity['used'])) {
 			if ($treatUsedAsInvalid) {
-				return false;
+				return null;
 			}
 			// return true and let the application check what to do then
-			return $res;
+			return $tokenEntity;
 		}
 		// actually spend key (set to used)
-		if ($this->spendKey($res['id'])) {
-			return $res;
+		if ($this->spendKey($tokenEntity['id'])) {
+			return $tokenEntity;
 		}
 		// no limit? we dont spend key then
-		if (!empty($res['unlimited'])) {
-			return $res;
+		if (!empty($tokenEntity['unlimited'])) {
+			return $tokenEntity;
 		}
-		return false;
+		return null;
 	}
 
 	/**

+ 1 - 1
src/Utility/Random.php

@@ -144,7 +144,7 @@ class Random {
 	 * @link https://github.com/CakeDC/users/blob/master/models/user.php#L498
 	 */
 	public static function pronounceablePwd($length = 10) {
-		srand((double)microtime() * 1000000);
+		srand((int)(double)microtime() * 1000000);
 		$password = '';
 		$vowels = ['a', 'e', 'i', 'o', 'u'];
 		$cons = ['b', 'c', 'd', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't', 'u', 'v', 'w', 'tr',

+ 1 - 1
src/Utility/Text.php

@@ -154,7 +154,7 @@ class Text extends CakeText {
 		if (mb_strlen($text) <= $length) {
 			return $text;
 		}
-		return rtrim(mb_substr($text, 0, round(($length - 3) / 2))) . $ending . ltrim(mb_substr($text, (($length - 3) / 2) * -1));
+		return rtrim(mb_substr($text, 0, (int)round(($length - 3) / 2))) . $ending . ltrim(mb_substr($text, (($length - 3) / 2) * -1));
 	}
 
 	/**

+ 1 - 1
src/Utility/Time.php

@@ -857,7 +857,7 @@ class Time extends CakeTime {
 
 				$x = mb_strtolower(mb_substr($format, $i, 1));
 
-				if ($str == 1) {
+				if ($str === 1) {
 					$ret .= $str . $s[$x];
 				} else {
 					$title = $p[$x];

+ 2 - 2
src/Utility/Utility.php

@@ -672,8 +672,8 @@ class Utility {
 	 * Returns microtime as float value
 	 * (to be subtracted right away)
 	 *
-	 * @param int $start
-	 * @param int $end
+	 * @param float $start
+	 * @param float $end
 	 * @param int $precision
 	 * @return float
 	 */

+ 0 - 2
src/View/Helper/FormatHelper.php

@@ -69,8 +69,6 @@ class FormatHelper extends Helper {
 	];
 
 	/**
-	 * FormatHelper constructor.
-	 *
 	 * @param \Cake\View\View $View
 	 * @param array $config
 	 */

+ 4 - 5
src/View/Helper/GravatarHelper.php

@@ -101,7 +101,7 @@ class GravatarHelper extends Helper {
 		unset($options['ext'], $options['secure']);
 		$protocol = $secure === true ? 'https' : 'http';
 
-		$imageUrl = $this->_url[$protocol] . md5($email);
+		$imageUrl = $this->_url[$protocol] . $this->_emailHash($email);
 		if ($ext === true) {
 			// If 'ext' option is supplied and true, append an extension to the generated image URL.
 			// This helps systems that don't display images unless they have a specific image extension on the URL.
@@ -122,7 +122,7 @@ class GravatarHelper extends Helper {
 		$images = [];
 		foreach ($this->_defaultIcons as $defaultIcon) {
 			$options['default'] = $defaultIcon;
-			$images[$defaultIcon] = $this->image(null, $options);
+			$images[$defaultIcon] = $this->image('', $options);
 		}
 		return $images;
 	}
@@ -158,11 +158,10 @@ class GravatarHelper extends Helper {
 	 * Generate email address hash
 	 *
 	 * @param string $email Email address
-	 * @param string $type Hash type to employ
 	 * @return string Email address hash
 	 */
-	protected function _emailHash($email, $type) {
-		return md5(mb_strtolower($email), $type);
+	protected function _emailHash($email) {
+		return md5(mb_strtolower($email));
 	}
 
 	/**

+ 137 - 0
src/View/Helper/ProgressHelper.php

@@ -0,0 +1,137 @@
+<?php
+
+namespace Tools\View\Helper;
+
+use Cake\Core\Configure;
+use Cake\View\Helper;
+use Cake\View\View;
+use InvalidArgumentException;
+use Tools\Utility\Number;
+
+/**
+ * @author Mark Scherer
+ * @license MIT
+ * @property \Cake\View\Helper\HtmlHelper $Html
+ */
+class ProgressHelper extends Helper {
+
+	const LENGTH_MIN = 3;
+	const CHAR_EMPTY = '░';
+	const CHAR_FULL = '█';
+
+	/**
+	 * @var array
+	 */
+	public $helpers = ['Html'];
+
+	/**
+	 * @var array
+	 */
+	protected $_defaults = [
+		'empty' => self::CHAR_EMPTY,
+		'full' => self::CHAR_FULL,
+	];
+
+	/**
+	 * @param \Cake\View\View $View
+	 * @param array $config
+	 */
+	public function __construct(View $View, array $config = []) {
+		$defaults = (array)Configure::read('Progress') + $this->_defaults;
+		$config += $defaults;
+
+		parent::__construct($View, $config);
+	}
+
+	/**
+	 * @param float $value Value 0...1
+	 * @param int $length As char count
+	 * @param array $attributes
+	 * @return string
+	 */
+	public function progressBar($value, $length, array $attributes = []) {
+		$bar = $this->draw($value, $length);
+
+		$attributes += [
+			'title' => Number::toPercentage($this->roundPercentage($value), 0, ['multiply' => true]),
+		];
+
+		return $this->Html->tag('span', $bar, $attributes);
+	}
+
+	/**
+	 * Render the progress bar based on the current state.
+	 *
+	 * @param float $complete
+	 * @param int $length
+	 * @return string
+	 * @throws \InvalidArgumentException
+	 */
+	public function draw($complete, $length) {
+		if ($complete < 0.0 || $complete > 1.0) {
+			throw new InvalidArgumentException('Min/Max overflow for value `' . $complete . '` (0...1)');
+		}
+		if ($length < static::LENGTH_MIN) {
+			throw new InvalidArgumentException('Min length for such a progress bar is ' . static::LENGTH_MIN);
+		}
+
+		$barLength = $this->calculateBarLength($complete, $length);
+
+		$bar = '';
+		if ($barLength > 0) {
+			$bar = str_repeat($this->getConfig('full'), $barLength);
+		}
+
+		$pad = $length - $barLength;
+		if ($pad > 0) {
+			$bar .= str_repeat($this->getConfig('empty'), $pad);
+		}
+
+		return $bar;
+	}
+
+	/**
+	 * @param float|int $total
+	 * @param float|int $is
+	 * @return float
+	 */
+	public function calculatePercentage($total, $is) {
+		$percentage = $total ? $is / $total : 0.0;
+
+		return $this->roundPercentage($percentage);
+	}
+
+	/**
+	 * @param float $percentage
+	 * @return float
+	 */
+	public function roundPercentage($percentage) {
+		$percentageRounded = round($percentage, 2);
+		if ($percentageRounded === 0.00 && $percentage > 0.0) {
+			$percentage = 0.01;
+		}
+		if ($percentageRounded === 1.00 && $percentage < 1.0) {
+			$percentage = 0.99;
+		}
+
+		return (float)$percentage;
+	}
+
+	/**
+	 * @param float $complete
+	 * @param int $length
+	 * @return int
+	 */
+	protected function calculateBarLength($complete, $length) {
+		$barLength = (int)round($length * $complete, 0);
+		if ($barLength === 0 && $complete > 0.0) {
+			$barLength = 1;
+		}
+		if ($barLength === $length && $complete < 1.0) {
+			$barLength = $length - 1;
+		}
+
+		return $barLength;
+	}
+
+}

+ 0 - 2
src/View/Helper/QrCodeHelper.php

@@ -73,8 +73,6 @@ class QrCodeHelper extends Helper {
 	public $formattingTypes = ['url' => 'http', 'tel' => 'tel', 'sms' => 'smsto', 'card' => 'mecard'];
 
 	/**
-	 * QrCodeHelper constructor.
-	 *
 	 * @param \Cake\View\View $View
 	 * @param array $config
 	 */

+ 0 - 2
src/View/Helper/TextHelper.php

@@ -30,8 +30,6 @@ if (!defined('CHAR_HELLIP')) {
 class TextHelper extends CakeTextHelper {
 
 	/**
-	 * Constructor
-	 *
 	 * ### Settings:
 	 *
 	 * - `engine` Class name to use to replace Text functionality.

+ 0 - 2
src/View/Helper/TimeHelper.php

@@ -39,8 +39,6 @@ class TimeHelper extends CakeTimeHelper {
 	protected $_engine;
 
 	/**
-	 * Default Constructor
-	 *
 	 * ### Settings:
 	 *
 	 * - `engine` Class name to use to replace Cake\I18n\Time functionality

+ 3 - 2
src/View/Helper/TimelineHelper.php

@@ -2,6 +2,7 @@
 
 namespace Tools\View\Helper;
 
+use Cake\I18n\FrozenTime;
 use Cake\View\Helper;
 
 /**
@@ -112,8 +113,8 @@ class TimelineHelper extends Helper {
 
 		$current = '';
 		if ($settings['current']) {
-			$dateString = date('Y-m-d H:i:s', time());
-			$current = 'timeline.setCurrentTime(' . $this->_date($dateString) . ');';
+			$now = new FrozenTime();
+			$current = 'timeline.setCurrentTime(' . $this->_date($now) . ');';
 		}
 		unset($settings['id']);
 		unset($settings['current']);

+ 9 - 14
tests/TestCase/BootstrapTest.php

@@ -19,8 +19,6 @@ class BootstrapTest extends TestCase {
 	}
 
 	/**
-	 * BootstrapTest::testStartsWith()
-	 *
 	 * @return void
 	 */
 	public function testStartsWith() {
@@ -49,7 +47,6 @@ class BootstrapTest extends TestCase {
 
 		foreach ($strings as $string) {
 			$is = startsWith($string[0], $string[1]);
-			//pr(returns($is). ' - expected '.returns($string[2]));
 			$this->assertEquals($string[2], $is);
 		}
 
@@ -58,8 +55,6 @@ class BootstrapTest extends TestCase {
 	}
 
 	/**
-	 * BootstrapTest::testEndsWith()
-	 *
 	 * @return void
 	 */
 	public function testEndsWith() {
@@ -97,8 +92,6 @@ class BootstrapTest extends TestCase {
 	}
 
 	/**
-	 * BootstrapTest::testContains()
-	 *
 	 * @return void
 	 */
 	public function testContains() {
@@ -139,26 +132,30 @@ class BootstrapTest extends TestCase {
 	 * @return void
 	 */
 	public function testEnt() {
-		//$this->assertEquals($expected, $is);
+		$result = ent('<>');
+		$expected = '&lt;&gt;';
+		$this->assertSame($expected, $result);
 	}
 
 	/**
 	 * @return void
 	 */
 	public function testEntDec() {
-		//$this->assertEquals($expected, $is);
+		$result = entDec('&lt;&gt;');
+		$expected = '<>';
+		$this->assertSame($expected, $result);
 	}
 
 	/**
 	 * @return void
 	 */
 	public function testReturns() {
-		//$this->assertEquals($expected, $is);
+		$result = returns([]);
+		$expected = '(array)';
+		$this->assertTextContains($expected, $result);
 	}
 
 	/**
-	 * BootstrapTest::testExtractPathInfo()
-	 *
 	 * @return void
 	 */
 	public function testExtractPathInfo() {
@@ -176,8 +173,6 @@ class BootstrapTest extends TestCase {
 	}
 
 	/**
-	 * BootstrapTest::testExtractFileInfo()
-	 *
 	 * @return void
 	 */
 	public function testExtractFileInfo() {

+ 64 - 0
tests/TestCase/Controller/ShuntRequestControllerTest.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace Tools\Test\TestCase\Controller;
+
+use Cake\Core\Configure;
+use Cake\TestSuite\IntegrationTestTrait;
+use RuntimeException;
+use Tools\TestSuite\TestCase;
+
+/**
+ * @uses \Tools\Controller\ShuntRequestController
+ */
+class ShuntRequestControllerTest extends TestCase {
+
+	use IntegrationTestTrait;
+
+	/**
+	 * @var array
+	 */
+	public $fixtures = [
+		'core.Sessions',
+	];
+
+	/**
+	 * @return void
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		Configure::write('Config.allowedLanguages', []);
+		Configure::write('Config.defaultLanguage', null);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testLanguage() {
+		$this->disableErrorHandlerMiddleware();
+
+		Configure::write('Config.defaultLanguage', 'de');
+		Configure::write('Config.allowedLanguages', [
+			'de' => [
+				'locale' => 'de_DE',
+				'name' => 'Deutsch'
+			],
+		]);
+
+		$this->post(['plugin' => 'Tools', 'controller' => 'ShuntRequest', 'action' => 'language']);
+
+		$this->assertRedirect();
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testLanguageError() {
+		$this->disableErrorHandlerMiddleware();
+
+		$this->expectException(RuntimeException::class);
+
+		$this->post(['plugin' => 'Tools', 'controller' => 'ShuntRequest', 'action' => 'language']);
+	}
+
+}

+ 4 - 3
tests/TestCase/Mailer/EmailTest.php

@@ -6,6 +6,7 @@ use App\Mailer\TestEmail;
 use Cake\Core\Configure;
 use Cake\Core\Plugin;
 use Cake\Log\Log;
+use Cake\Mailer\Message;
 use Cake\Mailer\TransportFactory;
 use Tools\Mailer\Email;
 use Tools\TestSuite\TestCase;
@@ -403,7 +404,7 @@ HTML;
 		$is = $this->Email->wrap($html);
 
 		foreach ($is as $line => $content) {
-			$this->assertTrue(strlen($content) <= Email::LINE_LENGTH_MUST);
+			$this->assertTrue(strlen($content) <= Message::LINE_LENGTH_MUST);
 		}
 		$this->debug($is);
 		$this->assertTrue(count($is) >= 5);
@@ -424,11 +425,11 @@ sjdf ojdshfdsf odsfh dsfhodsf hodshfhdsjdshfjdshfjdshfj dsjfh jdsfh ojds hfposjd
 </body></html>
 HTML;
 		//$html = str_replace(array("\r\n", "\n", "\r"), "", $html);
-		$this->Email->wrapLength(100);
+		$this->Email->setWrapLength(100);
 		$is = $this->Email->wrap($html);
 
 		foreach ($is as $line => $content) {
-			$this->assertTrue(strlen($content) <= Email::LINE_LENGTH_MUST);
+			$this->assertTrue(strlen($content) <= Message::LINE_LENGTH_MUST);
 		}
 		$this->debug($is);
 		$this->assertTrue(count($is) >= 16);

+ 2 - 2
tests/TestCase/Model/Behavior/PasswordableBehaviorTest.php

@@ -144,7 +144,7 @@ class PasswordableBehaviorTest extends TestCase {
 		} else {
 			$fields = ['name', 'id'];
 		}
-		$this->assertEquals($fields, $is->visibleProperties());
+		$this->assertEquals($fields, $is->getVisible());
 
 		$id = $user->id;
 		$user = $this->Users->newEmptyEntity();
@@ -161,7 +161,7 @@ class PasswordableBehaviorTest extends TestCase {
 		} else {
 			$fields = ['id'];
 		}
-		$this->assertEquals($fields, $is->visibleProperties());
+		$this->assertEquals($fields, $is->getVisible());
 	}
 
 	/**

+ 2 - 4
tests/TestCase/Model/Table/TokensTableTest.php

@@ -52,18 +52,16 @@ class TokensTableTest extends TestCase {
 		$this->assertTrue(!empty($key));
 
 		$res = $this->Tokens->useKey('test', $key);
-		//pr($res);
 		$this->assertTrue(!empty($res));
 
 		$res = $this->Tokens->useKey('test', $key);
-		//pr($res);
 		$this->assertTrue(!empty($res) && !empty($res['used']));
 
 		$res = $this->Tokens->useKey('test', $key . 'x');
-		$this->assertFalse($res);
+		$this->assertNull($res);
 
 		$res = $this->Tokens->useKey('testx', $key);
-		$this->assertFalse($res);
+		$this->assertNull($res);
 	}
 
 	/**

+ 5 - 15
tests/TestCase/Utility/TimeTest.php

@@ -655,42 +655,32 @@ class TimeTest extends TestCase {
 	}
 
 	/**
-	 * TimeTest::testLengthOfTime()
-	 *
 	 * @return void
 	 */
 	public function testLengthOfTime() {
-		//$this->out($this->_header(__FUNCTION__), true);
+		$ret = $this->Time->lengthOfTime(60, 'Hi');
+		$this->assertEquals('1 Minutes', $ret);
 
-		$ret = $this->Time->lengthOfTime(60);
-		//pr($ret);
+		$ret = $this->Time->lengthOfTime(DAY);
+		$this->assertEquals('1 Days, 0 Hours', $ret);
 
 		// FIX ME! Doesn't work!
 		$ret = $this->Time->lengthOfTime(-60);
-		//pr($ret);
-
-		$ret = $this->Time->lengthOfTime(-121);
-		//pr($ret);
+		//$this->assertEquals('-60 Seconds', $ret);
 
 		$this->assertEquals('6 ' . __d('tools', 'Minutes') . ', 40 ' . __d('tools', 'Seconds'), $this->Time->lengthOfTime(400));
 
 		$res = $this->Time->lengthOfTime(400, 'i');
-		//pr($res);
 		$this->assertEquals('6 ' . __d('tools', 'Minutes'), $res);
 
 		$res = $this->Time->lengthOfTime(6 * DAY);
-		//pr($res);
 		$this->assertEquals('6 ' . __d('tools', 'Days') . ', 0 ' . __d('tools', 'Hours'), $res);
 	}
 
 	/**
-	 * TimeTest::testFuzzyFromOffset()
-	 *
 	 * @return void
 	 */
 	public function testFuzzyFromOffset() {
-		//$this->out($this->_header(__FUNCTION__), true);
-
 		$ret = $this->Time->fuzzyFromOffset(MONTH);
 		//pr($ret);
 

+ 102 - 0
tests/TestCase/View/Helper/ProgressHelperTest.php

@@ -0,0 +1,102 @@
+<?php
+
+namespace Tools\Test\TestCase\View\Helper;
+
+use Cake\View\View;
+use Tools\TestSuite\TestCase;
+use Tools\View\Helper\ProgressHelper;
+
+class ProgressHelperTest extends TestCase {
+
+	/**
+	 * @var \Cake\View\View
+	 */
+	protected $View;
+
+	/**
+	 * @var \Tools\View\Helper\ProgressHelper
+	 */
+	protected $progressHelper;
+
+	/**
+	 * @return void
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$this->View = new View(null);
+		$this->progressHelper = new ProgressHelper($this->View);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testDraw() {
+		$result = $this->progressHelper->draw(0.00, 3);
+		$this->assertSame('░░░', $result);
+
+		$result = $this->progressHelper->draw(1.00, 3);
+		$this->assertSame('███', $result);
+
+		$result = $this->progressHelper->draw(0.50, 3);
+		$this->assertSame('██░', $result);
+
+		$result = $this->progressHelper->draw(0.30, 5);
+		$this->assertSame('██░░░', $result);
+
+		$result = $this->progressHelper->draw(0.01, 3);
+		$this->assertSame('█░░', $result);
+
+		$result = $this->progressHelper->draw(0.99, 3);
+		$this->assertSame('██░', $result);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testProgressBar() {
+		$result = $this->progressHelper->progressBar(0.001, 3);
+		$this->assertSame('<span title="1%">█░░</span>', $result);
+
+		$result = $this->progressHelper->progressBar(0.999, 3);
+		$this->assertSame('<span title="99%">██░</span>', $result);
+
+		$result = $this->progressHelper->progressBar(0.000, 3);
+		$this->assertSame('<span title="0%">░░░</span>', $result);
+
+		$result = $this->progressHelper->progressBar(1.000, 3);
+		$this->assertSame('<span title="100%">███</span>', $result);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testCalculatePercentage() {
+		$result = $this->progressHelper->calculatePercentage(0, 0);
+		$this->assertSame(0.00, $result);
+
+		$result = $this->progressHelper->calculatePercentage(0.0, 0.0);
+		$this->assertSame(0.00, $result);
+
+		$result = $this->progressHelper->calculatePercentage(997, 1);
+		$this->assertSame(0.01, $result);
+
+		$result = $this->progressHelper->calculatePercentage(997, 996);
+		$this->assertSame(0.99, $result);
+
+		$result = $this->progressHelper->calculatePercentage(997, 997);
+		$this->assertSame(1.00, $result);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testRoundPercentage() {
+		$result = $this->progressHelper->roundPercentage(0.001);
+		$this->assertSame(0.01, $result);
+
+		$result = $this->progressHelper->roundPercentage(0.999);
+		$this->assertSame(0.99, $result);
+	}
+
+}

+ 1 - 2
tests/bootstrap.php

@@ -27,7 +27,7 @@ define('CAKE', CORE_PATH . APP_DIR . DS);
 define('TESTS', ROOT . DS . 'tests' . DS);
 
 define('WWW_ROOT', ROOT . DS . 'webroot' . DS);
-define('CONFIG', dirname(__FILE__) . DS . 'config' . DS);
+//define('CONFIG', dirname(__FILE__) . DS . 'config' . DS);
 
 ini_set('intl.default_locale', 'de-DE');
 
@@ -91,7 +91,6 @@ Router::defaultRouteClass(DashedRoute::class);
 Router::reload();
 require TESTS . 'config' . DS . 'routes.php';
 
-//Cake\Core\Plugin::load('Tools', ['path' => ROOT . DS, 'bootstrap' => true]);
 Cake\Core\Plugin::getCollection()->add(new Tools\Plugin());
 
 if (getenv('db_dsn')) {

+ 5 - 3
tests/config/routes.php

@@ -4,10 +4,12 @@ namespace Tools\Test\App\Config;
 
 use Cake\Routing\RouteBuilder;
 use Cake\Routing\Router;
+use Cake\Routing\Route\DashedRoute;
+
+Router::defaultRouteClass(DashedRoute::class);
 
 Router::scope('/', function(RouteBuilder $routes) {
 	$routes->fallbacks();
 });
-Router::plugin('Tools', function (RouteBuilder $routes) {
-	$routes->fallbacks();
-});
+
+require ROOT . DS . 'config' . DS . 'routes.php';

+ 2 - 1
tests/test_app/Mailer/TestEmail.php

@@ -1,6 +1,7 @@
 <?php
 namespace App\Mailer;
 
+use Cake\Mailer\Message;
 use Tools\Mailer\Email;
 
 /**
@@ -15,7 +16,7 @@ class TestEmail extends Email {
 	 * @param int $length
 	 * @return array
 	 */
-	public function wrap($text, $length = Email::LINE_LENGTH_MUST) {
+	public function wrap($text, $length = Message::LINE_LENGTH_MUST) {
 		return parent::_wrap($text, $length);
 	}
 

+ 4 - 4
tests/test_app/Model/Table/DataTable.php

@@ -2,17 +2,17 @@
 
 namespace App\Model\Table;
 
-use Cake\Database\Schema\TableSchema;
+use Cake\Database\Schema\TableSchemaInterface;
 use Tools\Model\Table\Table;
 
 class DataTable extends Table {
 
 	/**
-	 * @param \Cake\Database\Schema\TableSchema $schema
+	 * @param \Cake\Database\Schema\TableSchemaInterface $schema
 	 *
-	 * @return \Cake\Database\Schema\TableSchema
+	 * @return \Cake\Database\Schema\TableSchemaInterface
 	 */
-	protected function _initializeSchema(TableSchema $schema): TableSchema {
+	protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaInterface {
 		$schema->setColumnType('data_json', 'json');
 		$schema->setColumnType('data_array', 'array');