Fixed critical security bug, risking unprotected views to be exposed.

Had to make some design concessions. ViewType can no longer be distilled using the routeMatches, but must instead now be provided by the more static routeConfig. This way, the chosen ViewType shall always be one that is chosen by the developer.

Also added some other smaller features, as requested by other components and plugins:
- Now added a 'namespacePrefix' for Router::defaultCallable. Allows for the usage of Views and Controllers from a namespace different from '\\Application', from within the router sphere.
- Added a $routes = [] parameter to Router::route(), allowing the developer to bypass the global routes list if necessary.
This commit is contained in:
Abel Hoogeveen 2021-01-19 21:16:39 +01:00
parent 55e0848a70
commit 9d86c03f02
Signed by: abelhooge
GPG Key ID: 387E8DC1F73306FC
17 changed files with 323 additions and 235 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ composer.phar
build/
test/temp/
vendor/
*.cache

View File

@ -1,82 +0,0 @@
before_script:
# Install dependencies
- set -xe
- apt-get update -yqq
- apt-get install git zip unzip -yqq
stages:
- build
- test
- deploy
build:composer:
image: php:7.2
stage: build
script:
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install
cache:
key: "$CI_BUILD_REF_$CI_BUILD_REF_NAME"
paths:
- vendor/
test:7.1:
stage: test
image: php:7.1
script:
- vendor/bin/phpunit -c test/phpunit.xml
cache:
key: "$CI_BUILD_REF_$CI_BUILD_REF_NAME"
paths:
- vendor
test:7.2:
stage: test
image: php:7.2
script:
- vendor/bin/phpunit -c test/phpunit.xml
cache:
key: "$CI_BUILD_REF_$CI_BUILD_REF_NAME"
paths:
- vendor/
test:7.3:
stage: test
image: php:7.3
script:
- vendor/bin/phpunit -c test/phpunit.xml
cache:
key: "$CI_BUILD_REF_$CI_BUILD_REF_NAME"
paths:
- vendor/
test:coverage:
stage: test
image: php:7.2
script:
- pecl install xdebug
- docker-php-ext-enable xdebug
- vendor/bin/phpunit -c test/phpunit.xml --coverage-text
cache:
key: "$CI_BUILD_REF_$CI_BUILD_REF_NAME"
paths:
- vendor/
release:
stage: deploy
image: php:7.2
only:
- master
script:
- pecl install xdebug
- docker-php-ext-enable xdebug
- vendor/bin/phpunit -c test/phpunit.xml --coverage-text
artifacts:
name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}"
paths:
- build/
expire_in: 3 weeks
cache:
key: "$CI_BUILD_REF_$CI_BUILD_REF_NAME"
paths:
- vendor/

View File

@ -1,12 +0,0 @@
language: php
php:
- 7.1
- 7.2
- 7.3
script:
- php vendor/bin/phpunit -v -c test/phpunit.xml --coverage-text
before_script:
- composer install

View File

@ -13,12 +13,12 @@
}
],
"require": {
"php": ">=7.3.0",
"php": ">=7.4.0",
"fuzeworks/core": "~1.2.0"
},
"require-dev": {
"phpunit/phpunit": "^7",
"mikey179/vfsstream": "1.6.5"
"phpunit/phpunit": "^9",
"mikey179/vfsstream": "~1.6.0"
},
"autoload": {
"psr-4": {

View File

@ -39,5 +39,6 @@ return array(
'default_view' => 'index',
'default_viewType' => 'standard',
'default_viewMethod' => 'index',
'default_namespacePrefix' => '\Application\\'
);

View File

@ -65,6 +65,13 @@ class RouterLoadCallableEvent extends Event
*/
public $matches;
/**
* The static route configuration
*
* @var array
*/
public $routeData;
/**
* The route which resulted in this callable being loaded
*
@ -72,10 +79,11 @@ class RouterLoadCallableEvent extends Event
*/
public $route;
public function init(callable $callable, array $matches, string $route)
public function init(callable $callable, array $matches, array $routeData, string $route)
{
$this->callable = $callable;
$this->matches = $matches;
$this->routeData = $routeData;
$this->route = $route;
}

View File

@ -79,6 +79,13 @@ class RouterLoadViewAndControllerEvent extends Event
*/
public $viewParameters;
/**
* The namespace to use to load the View and Controller
*
* @var string
*/
public $namespacePrefix;
/**
* The route that resulted in this controller and view
*
@ -93,12 +100,13 @@ class RouterLoadViewAndControllerEvent extends Event
*/
public $controller;
public function init(string $viewName, string $viewType, array $viewMethods, string $viewParameters, string $route)
public function init(string $viewName, string $viewType, array $viewMethods, string $viewParameters, string $namespacePrefix, string $route)
{
$this->viewName = $viewName;
$this->viewType = $viewType;
$this->viewMethods = $viewMethods;
$this->viewParameters = $viewParameters;
$this->namespacePrefix = $namespacePrefix;
$this->route = $route;
}

View File

@ -70,6 +70,11 @@ class Router
*/
protected $matches = null;
/**
* @var array|null
*/
protected $routeData = null;
/**
* The current route used
*
@ -198,19 +203,23 @@ class Router
/**
* @param string $path
* @param array $routes Optional routes for not using the default
* @return mixed
* @throws NotFoundException
* @throws RouterException
* @throws HaltException
*/
public function route(string $path)
public function route(string $path, array $routes = [])
{
// Select the routes to use
$routes = empty($routes) ? $this->routes : $routes;
// Check all the provided custom paths, ordered by priority
for ($i=Priority::getHighestPriority(); $i<=Priority::getLowestPriority(); $i++) {
if (!isset($this->routes[$i]))
if (!isset($routes[$i]))
continue;
foreach ($this->routes[$i] as $route => $routeConfig)
foreach ($routes[$i] as $route => $routeConfig)
{
// Match the path against the routes
if (!preg_match('#^'.$route.'$#', $path, $matches))
@ -219,29 +228,30 @@ class Router
// Save the matches
Logger::log("Route matched: '" . $route . "' with " . Priority::getPriority($i));
$this->matches = $matches;
$this->routeData = $routeConfig;
$this->route = $route;
$this->callable = null;
// Call callable if routeConfig is callable, so routeConfig can be replaced
// This is an example of 'Dynamic Rewrite'
// e.g: '.*$' => callable
if (is_callable($routeConfig))
$routeConfig = call_user_func_array($routeConfig, [$matches]);
if (is_callable($this->routeData))
$this->routeData = call_user_func_array($this->routeData, [$matches]);
// If routeConfig is an array, multiple things might be at hand
if (is_array($routeConfig))
if (is_array($this->routeData))
{
// Replace defaultCallable if a custom callable is provided
// This is an example of 'Custom Callable'
// e.g: '.*$' => ['callable' => [$object, 'method']]
if (isset($routeConfig['callable']) && is_callable($routeConfig['callable']))
$this->callable = $routeConfig['callable'];
if (isset($this->routeData['callable']) && is_callable($this->routeData['callable']))
$this->callable = $this->routeData['callable'];
// If the route provides a configuration, use that
// This is an example of 'Static Rewrite'
// e.g: '.*$' => ['viewName' => 'custom', 'viewType' => 'cli', 'function' => 'index']
else
$this->matches = array_merge($this->matches, $routeConfig);
$this->matches = array_merge($this->matches, $this->routeData);
}
// If no custom callable is provided, use default
@ -250,7 +260,7 @@ class Router
$this->callable = [$this, 'defaultCallable'];
// Attempt and load callable. If false, continue
$output = $this->loadCallable($this->callable, $this->matches, $route);
$output = $this->loadCallable($this->callable, $this->matches, $this->routeData, $route);
if (is_bool($output) && $output === FALSE)
{
Logger::log('Callable not satisfied, skipping to next callable');
@ -267,12 +277,13 @@ class Router
/**
* @param callable $callable
* @param array $matches
* @param array $routeData
* @param string $route
* @return mixed
* @throws RouterException
* @throws HaltException
* @throws RouterException
*/
protected function loadCallable(callable $callable, array $matches, string $route)
protected function loadCallable(callable $callable, array $matches, array $routeData, string $route)
{
// Log the input to the logger
Logger::newLevel('Loading callable with matches:');
@ -286,6 +297,7 @@ class Router
$event = Events::fireEvent('routerLoadCallableEvent',
$callable,
$matches,
$routeData,
$route
);
} catch (EventException $e) {
@ -297,29 +309,34 @@ class Router
throw new HaltException("Will not load callable. Cancelled by routerLoadCallableEvent.");
// Invoke callable
$output = call_user_func_array($event->callable, [$event->matches, $event->route]);
$output = call_user_func_array($event->callable, [$event->matches, $event->routeData, $event->route]);
Logger::stopLevel();
return $output;
}
/**
* @param array $matches
* @param array $routeData
* @param string $route
* @return mixed
* @throws HaltException
* @throws RouterException
* @todo Use $route and send it to the view
*/
public function defaultCallable(array $matches, string $route)
public function defaultCallable(array $matches, array $routeData, string $route)
{
Logger::log('defaultCallable called');
// Prepare variables
// Variables from matches
$viewName = !empty($matches['viewName']) ? $matches['viewName'] : $this->config->routing->default_view;
$viewType = !empty($matches['viewType']) ? $matches['viewType'] : $this->config->routing->default_viewType;
$viewMethod = !empty($matches['viewMethod']) ? $matches['viewMethod'] : $this->config->routing->default_viewMethod;
$viewParameters = !empty($matches['viewParameters']) ? $matches['viewParameters'] : '';
// Variables from routeData
$viewType = !empty($routeData['viewType']) ? $routeData['viewType'] : $this->config->routing->default_viewType;
$namespacePrefix = !empty($routeData['namespacePrefix']) ? $routeData['namespacePrefix'] : $this->config->routing->default_namespacePrefix;
try {
/** @var RouterLoadViewAndControllerEvent $event */
$event = Events::fireEvent('routerLoadViewAndControllerEvent',
@ -328,6 +345,7 @@ class Router
// ViewMethod is provided as a Priority::NORMAL method
[3 => [$viewMethod]],
$viewParameters,
$namespacePrefix,
$route
);
} catch (EventException $e) {
@ -340,7 +358,7 @@ class Router
// First receive the controller
try {
$this->controller = (!is_null($event->controller) ? $event->controller : $this->controllers->get($event->viewName));
$this->controller = (!is_null($event->controller) ? $event->controller : $this->controllers->get($event->viewName, [], $event->namespacePrefix . 'Controller\\'));
} catch (ControllerException $e) {
throw new RouterException("Could not load view. Controllers::get threw ControllerException: '".$e->getMessage()."'");
} catch (NotFoundException $e) {
@ -350,7 +368,7 @@ class Router
// Then try and receive the view
try {
$this->view = $this->views->get($event->viewName, $this->controller, $event->viewType);
$this->view = $this->views->get($event->viewName, $this->controller, $event->viewType, [], $event->namespacePrefix. 'View\\');
} catch (ViewException $e) {
throw new RouterException("Could not load view. Views::get threw ViewException: '".$e->getMessage()."'");
} catch (NotFoundException $e) {

View File

@ -38,10 +38,7 @@
chdir(dirname(__DIR__));
// Load the FuzeWorks container
$container = require('test/bootstrap.php');
// Load the test abstract
require_once 'mcr/MVCRTestAbstract.php';
$container = require('bootstrap.php');
// Reset error and exception handlers
restore_error_handler();

View File

@ -44,7 +44,6 @@ $configurator->setLogDirectory(dirname(__FILE__) . '/temp');
// Other values
$configurator->setTimeZone('Europe/Amsterdam');
$configurator->enableDebugMode(false);
$configurator->setDebugAddress('NONE');
// Implement the MVCR Component

View File

@ -0,0 +1,44 @@
<?php
/**
* FuzeWorks Framework MVCR Component.
*
* The FuzeWorks PHP FrameWork
*
* Copyright (C) 2013-2018 TechFuze
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* @author TechFuze
* @copyright Copyright (c) 2013 - 2018, TechFuze. (http://techfuze.net)
* @license https://opensource.org/licenses/MIT MIT License
*
* @link http://techfuze.net/fuzeworks
* @since Version 1.2.0
*
* @version Version 1.2.0
*/
namespace Custom\Controller;
use FuzeWorks\Controller;
class TestDefaultCallableCustomNamespaceController extends Controller
{
}

View File

@ -38,24 +38,39 @@ use FuzeWorks\Controller;
use FuzeWorks\Controllers;
use FuzeWorks\Event\ControllerGetEvent;
use FuzeWorks\Events;
use FuzeWorks\Exception\ControllerException;
use FuzeWorks\Exception\NotFoundException;
use FuzeWorks\Factory;
use FuzeWorks\Priority;
use PHPUnit\Framework\TestCase;
/**
* Class ControllersTest
* @coversDefaultClass \FuzeWorks\Controllers
*/
class ControllersTest extends MVCRTestAbstract
class ControllersTest extends TestCase
{
/**
* @var Controllers
*/
protected $controllers;
public function setUp()
public function setUp(): void
{
$this->controllers = new Controllers();
$this->controllers->addComponentPath('test'.DS.'controllers');
$this->controllers->addComponentPath('controllers');
}
/**
* Remove all listeners before the next test starts.
*/
public function tearDown(): void
{
// Clear all events created by tests
Events::$listeners = array();
// Reset all config files
Factory::getInstance()->config->discardConfigFiles();
}
/**
@ -77,7 +92,6 @@ class ControllersTest extends MVCRTestAbstract
* @depends testGetControllerFromClass
* @covers ::get
* @covers ::loadController
* @expectedException \FuzeWorks\Exception\ControllerException
*/
public function testGetControllerFromClassInvalidInstance()
{
@ -87,6 +101,7 @@ class ControllersTest extends MVCRTestAbstract
class_alias($mockFakeControllerClass, $mockFakeControllerClass . 'Controller');
// Try and fetch
$this->expectException(ControllerException::class);
$this->controllers->get($mockFakeControllerClass, [], '\\');
}
@ -120,10 +135,10 @@ class ControllersTest extends MVCRTestAbstract
/**
* @covers ::get
* @expectedException \FuzeWorks\Exception\ControllerException
*/
public function testGetControllerInvalidName()
{
$this->expectException(ControllerException::class);
$this->controllers->get('', [], '\\');
}
@ -141,10 +156,10 @@ class ControllersTest extends MVCRTestAbstract
* @depends testGetControllerFromFile
* @covers ::get
* @covers ::loadController
* @expectedException \FuzeWorks\Exception\ControllerException
*/
public function testGetControllerFromFileInvalidInstance()
{
$this->expectException(ControllerException::class);
$this->controllers->get('ControllerInvalidInstance');
}
@ -156,8 +171,8 @@ class ControllersTest extends MVCRTestAbstract
public function testDifferentComponentPathPriority()
{
// Add the directories for this test
$this->controllers->addComponentPath('test'.DS.'controllers'.DS.'TestDifferentComponentPathPriority'.DS.'Lowest', Priority::LOWEST);
$this->controllers->addComponentPath('test'.DS.'controllers'.DS.'TestDifferentComponentPathPriority'.DS.'Highest', Priority::HIGHEST);
$this->controllers->addComponentPath('controllers'.DS.'TestDifferentComponentPathPriority'.DS.'Lowest', Priority::LOWEST);
$this->controllers->addComponentPath('controllers'.DS.'TestDifferentComponentPathPriority'.DS.'Highest', Priority::HIGHEST);
// Load the controller and assert it is the correct type
$controller = $this->controllers->get('TestDifferentComponentPathPriority');
@ -182,10 +197,10 @@ class ControllersTest extends MVCRTestAbstract
* @depends testGetControllerFromFile
* @covers ::get
* @covers ::loadController
* @expectedException \FuzeWorks\Exception\NotFoundException
*/
public function testControllerNotFound()
{
$this->expectException(NotFoundException::class);
$this->controllers->get('NotFound');
}
@ -193,7 +208,6 @@ class ControllersTest extends MVCRTestAbstract
* @depends testGetControllerFromClass
* @covers ::get
* @covers \FuzeWorks\Event\ControllerGetEvent::init
* @expectedException \FuzeWorks\Exception\ControllerException
*/
public function testControllerGetEvent()
{
@ -208,13 +222,13 @@ class ControllersTest extends MVCRTestAbstract
$event->setCancelled(true);
}, 'controllerGetEvent', Priority::NORMAL);
$this->expectException(ControllerException::class);
$this->controllers->get('SomeControllerName', ['some_path'], 'SomeNamespace', 'Some Argument');
}
/**
* @depends testControllerGetEvent
* @covers ::get
* @expectedException \FuzeWorks\Exception\ControllerException
*/
public function testCancelGetController()
{
@ -223,6 +237,7 @@ class ControllersTest extends MVCRTestAbstract
$event->setCancelled(true);
}, 'controllerGetEvent', Priority::NORMAL);
$this->expectException(ControllerException::class);
$this->controllers->get('SomeController', [], '\\');
}

View File

@ -36,15 +36,19 @@
use FuzeWorks\Event\ModelGetEvent;
use FuzeWorks\Events;
use FuzeWorks\Exception\ModelException;
use FuzeWorks\Exception\NotFoundException;
use FuzeWorks\Factory;
use FuzeWorks\Model;
use FuzeWorks\Models;
use FuzeWorks\Priority;
use PHPUnit\Framework\TestCase;
/**
* Class ModelsTest
* @coversDefaultClass \FuzeWorks\Models
*/
class ModelsTest extends MVCRTestAbstract
class ModelsTest extends TestCase
{
/**
@ -52,10 +56,22 @@ class ModelsTest extends MVCRTestAbstract
*/
protected $models;
public function setUp()
public function setUp(): void
{
$this->models = new Models();
$this->models->addComponentPath('test'.DS.'models');
$this->models->addComponentPath('models');
}
/**
* Remove all listeners before the next test starts.
*/
public function tearDown(): void
{
// Clear all events created by tests
Events::$listeners = array();
// Reset all config files
Factory::getInstance()->config->discardConfigFiles();
}
/**
@ -77,7 +93,6 @@ class ModelsTest extends MVCRTestAbstract
* @depends testGetModelFromClass
* @covers ::get
* @covers ::loadModel
* @expectedException \FuzeWorks\Exception\ModelException
*/
public function testGetModelFromClassInvalidInstance()
{
@ -87,6 +102,7 @@ class ModelsTest extends MVCRTestAbstract
class_alias($mockFakeModelClass, $mockFakeModelClass . 'Model');
// Try and fetch
$this->expectException(ModelException::class);
$this->models->get($mockFakeModelClass, [], '\\');
}
@ -120,10 +136,10 @@ class ModelsTest extends MVCRTestAbstract
/**
* @covers ::get
* @expectedException \FuzeWorks\Exception\ModelException
*/
public function testGetModelInvalidName()
{
$this->expectException(ModelException::class);
$this->models->get('', [], '\\');
}
@ -141,10 +157,10 @@ class ModelsTest extends MVCRTestAbstract
* @depends testGetModelFromFile
* @covers ::get
* @covers ::loadModel
* @expectedException \FuzeWorks\Exception\ModelException
*/
public function testGetModelFromFileInvalidInstance()
{
$this->expectException(ModelException::class);
$this->models->get('ModelInvalidInstance');
}
@ -156,8 +172,8 @@ class ModelsTest extends MVCRTestAbstract
public function testDifferentComponentPathPriority()
{
// Add the directories for this test
$this->models->addComponentPath('test'.DS.'models'.DS.'TestDifferentComponentPathPriority'.DS.'Lowest', Priority::LOWEST);
$this->models->addComponentPath('test'.DS.'models'.DS.'TestDifferentComponentPathPriority'.DS.'Highest', Priority::HIGHEST);
$this->models->addComponentPath('models'.DS.'TestDifferentComponentPathPriority'.DS.'Lowest', Priority::LOWEST);
$this->models->addComponentPath('models'.DS.'TestDifferentComponentPathPriority'.DS.'Highest', Priority::HIGHEST);
// Load the model and assert it is the correct type
$model = $this->models->get('TestDifferentComponentPathPriority');
@ -182,10 +198,10 @@ class ModelsTest extends MVCRTestAbstract
* @depends testGetModelFromFile
* @covers ::get
* @covers ::loadModel
* @expectedException \FuzeWorks\Exception\NotFoundException
*/
public function testModelNotFound()
{
$this->expectException(NotFoundException::class);
$this->models->get('NotFound');
}
@ -193,7 +209,6 @@ class ModelsTest extends MVCRTestAbstract
* @depends testGetModelFromClass
* @covers ::get
* @covers \FuzeWorks\Event\ModelGetEvent::init
* @expectedException \FuzeWorks\Exception\ModelException
*/
public function testModelGetEvent()
{
@ -208,13 +223,13 @@ class ModelsTest extends MVCRTestAbstract
$event->setCancelled(true);
}, 'modelGetEvent', Priority::NORMAL);
$this->expectException(ModelException::class);
$this->models->get('SomeModelName', ['some_path'], 'SomeNamespace', 'Some Argument');
}
/**
* @depends testModelGetEvent
* @covers ::get
* @expectedException \FuzeWorks\Exception\ModelException
*/
public function testCancelGetModel()
{
@ -223,6 +238,7 @@ class ModelsTest extends MVCRTestAbstract
$event->setCancelled(true);
}, 'modelGetEvent', Priority::NORMAL);
$this->expectException(ModelException::class);
$this->models->get('SomeModel', [], '\\');
}

View File

@ -36,15 +36,18 @@
use FuzeWorks\Config;
use FuzeWorks\Events;
use FuzeWorks\Exception\HaltException;
use FuzeWorks\Exception\NotFoundException;
use FuzeWorks\Factory;
use FuzeWorks\Priority;
use FuzeWorks\Router;
use PHPUnit\Framework\TestCase;
/**
* Class RouterTest
* @coversDefaultClass \FuzeWorks\Router
*/
class RouterTest extends MVCRTestAbstract
class RouterTest extends TestCase
{
/**
@ -61,15 +64,27 @@ class RouterTest extends MVCRTestAbstract
*/
protected $config;
public function setUp()
public function setUp(): void
{
// Get required classes
$this->router = new Router();
$this->config = Factory::getInstance()->config;
// Append required routes
Factory::getInstance()->controllers->addComponentPath('test' . DS . 'controllers');
Factory::getInstance()->views->addComponentPath('test' . DS . 'views');
Factory::getInstance()->controllers->addComponentPath('controllers');
Factory::getInstance()->views->addComponentPath('views');
}
/**
* Remove all listeners before the next test starts.
*/
public function tearDown(): void
{
// Clear all events created by tests
Events::$listeners = array();
// Reset all config files
Factory::getInstance()->config->discardConfigFiles();
}
/**
@ -203,13 +218,16 @@ class RouterTest extends MVCRTestAbstract
{
$matches = [
'viewName' => 'TestDefaultCallable',
'viewType' => 'test',
'viewMethod' => 'someMethod'
];
$data = [
'viewType' => 'test'
];
$this->assertNull($this->router->getCurrentController());
$this->assertNull($this->router->getCurrentView());
$this->assertEquals('Verify Output', $this->router->defaultCallable($matches, '.*$'));
$this->assertEquals('Verify Output', $this->router->defaultCallable($matches, $data, '.*$'));
$this->assertInstanceOf('\Application\Controller\TestDefaultCallableController', $this->router->getCurrentController());
$this->assertInstanceOf('\Application\View\TestDefaultCallableTestView', $this->router->getCurrentView());
}
@ -222,13 +240,16 @@ class RouterTest extends MVCRTestAbstract
{
$matches = [
'viewName' => 'TestDefaultCallable',
'viewType' => 'test',
'viewMethod' => 'missing'
];
$data = [
'viewType' => 'test'
];
$this->assertNull($this->router->getCurrentController());
$this->assertNull($this->router->getCurrentView());
$this->assertFalse($this->router->defaultCallable($matches, '.*$'));
$this->assertFalse($this->router->defaultCallable($matches, $data, '.*$'));
$this->assertInstanceOf('\Application\Controller\TestDefaultCallableController', $this->router->getCurrentController());
$this->assertInstanceOf('\Application\View\TestDefaultCallableTestView', $this->router->getCurrentView());
}
@ -241,13 +262,16 @@ class RouterTest extends MVCRTestAbstract
{
$matches = [
'viewName' => 'TestDefaultCallableMissingView',
'viewType' => 'test',
'viewMethod' => 'missing'
];
$data = [
'viewType' => 'test'
];
$this->assertNull($this->router->getCurrentController());
$this->assertNull($this->router->getCurrentView());
$this->assertFalse($this->router->defaultCallable($matches, '.*$'));
$this->assertFalse($this->router->defaultCallable($matches, $data,'.*$'));
$this->assertInstanceOf('\Application\Controller\TestDefaultCallableMissingViewController', $this->router->getCurrentController());
$this->assertNull($this->router->getCurrentView());
}
@ -260,13 +284,16 @@ class RouterTest extends MVCRTestAbstract
{
$matches = [
'viewName' => 'TestDefaultCallableMissingController',
'viewType' => 'test',
'viewMethod' => 'missing'
];
$data = [
'viewType' => 'test'
];
$this->assertNull($this->router->getCurrentController());
$this->assertNull($this->router->getCurrentView());
$this->assertFalse($this->router->defaultCallable($matches, '.*$'));
$this->assertFalse($this->router->defaultCallable($matches, $data, '.*$'));
$this->assertNull($this->router->getCurrentController());
$this->assertNull($this->router->getCurrentView());
}
@ -274,19 +301,23 @@ class RouterTest extends MVCRTestAbstract
/**
* @depends testDefaultCallable
* @covers ::defaultCallable
* @expectedException \FuzeWorks\Exception\HaltException
*/
public function testDefaultCallableHaltByView()
{
$matches = [
'viewName' => 'TestDefaultCallableHalt',
'viewType' => 'test',
'viewMethod' => 'someMethod'
];
$data = [
'viewType' => 'test'
];
$this->assertNull($this->router->getCurrentController());
$this->assertNull($this->router->getCurrentView());
$this->router->defaultCallable($matches, '.*$');
$this->expectException(HaltException::class);
$this->router->defaultCallable($matches, $data,'.*$');
$this->assertInstanceOf('\Application\Controller\TestDefaultCallableHaltController', $this->router->getCurrentController());
$this->assertInstanceOf('\Application\View\TestDefaultCallableHaltTestView', $this->router->getCurrentView());
}
@ -298,31 +329,59 @@ class RouterTest extends MVCRTestAbstract
public function testDefaultCallableEmptyName()
{
$matches = [
'viewType' => 'test',
'viewMethod' => 'someMethod'
];
$data = [
'viewType' => 'test'
];
$this->assertNull($this->router->getCurrentController());
$this->assertNull($this->router->getCurrentView());
$this->assertFalse($this->router->defaultCallable($matches, '.*$'));
$this->assertFalse($this->router->defaultCallable($matches, $data, '.*$'));
$this->assertNull($this->router->getCurrentController());
$this->assertNull($this->router->getCurrentView());
}
/**
* @depends testDefaultCallable
* @covers ::defaultCallable
*/
public function testDefaultCallableCustomNamespace()
{
$matches = [
'viewName' => 'TestDefaultCallableCustomNamespace',
'viewMethod' => 'someMethod'
];
$data = [
'viewType' => 'test',
'namespacePrefix' => '\Custom\\'
];
$this->assertNull($this->router->getCurrentController());
$this->assertNull($this->router->getCurrentView());
$this->assertEquals('Verify Output, with custom namespace!', $this->router->defaultCallable($matches, $data, '.*$'));
$this->assertInstanceOf('\Custom\Controller\TestDefaultCallableCustomNamespaceController', $this->router->getCurrentController());
$this->assertInstanceOf('\Custom\View\TestDefaultCallableCustomNamespaceTestView', $this->router->getCurrentView());
}
/**
* @depends testDefaultCallable
* @covers ::defaultCallable
* @covers \FuzeWorks\Event\RouterLoadViewAndControllerEvent::init
* @expectedException \FuzeWorks\Exception\HaltException
*/
public function testDefaultCallableHaltByEvent()
{
$matches = [
'viewName' => 'TestDefaultCallable',
'viewType' => 'test',
'viewMethod' => 'missing'
];
$data = [
'viewType' => 'test'
];
$this->assertNull($this->router->getCurrentController());
$this->assertNull($this->router->getCurrentView());
@ -335,7 +394,8 @@ class RouterTest extends MVCRTestAbstract
$event->setCancelled(true);
}, 'routerLoadViewAndControllerEvent');
$this->router->defaultCallable($matches, '.*$');
$this->expectException(HaltException::class);
$this->router->defaultCallable($matches, $data, '.*$');
}
/**
@ -347,10 +407,13 @@ class RouterTest extends MVCRTestAbstract
{
$matches = [
'viewName' => 'TestDefaultCallable',
'viewType' => 'test',
'viewMethod' => 'missing'
];
$data = [
'viewType' => 'test'
];
$this->assertNull($this->router->getCurrentController());
$this->assertNull($this->router->getCurrentView());
@ -361,7 +424,7 @@ class RouterTest extends MVCRTestAbstract
$event->overrideController($mockController);
}, 'routerLoadViewAndControllerEvent', Priority::NORMAL, $mockController);
$this->router->defaultCallable($matches, '.*$');
$this->router->defaultCallable($matches, $data, '.*$');
$this->assertEquals($mockController, $this->router->getCurrentController());
}
@ -374,10 +437,13 @@ class RouterTest extends MVCRTestAbstract
{
$matches = [
'viewName' => 'TestDefaultCallableChangeMethod',
'viewType' => 'test',
'viewMethod' => 'index'
];
$data = [
'viewType' => 'test'
];
$this->assertNull($this->router->getCurrentController());
$this->assertNull($this->router->getCurrentView());
@ -388,7 +454,7 @@ class RouterTest extends MVCRTestAbstract
$event->addMethod('altered', Priority::HIGH);
}, 'routerLoadViewAndControllerEvent', Priority::NORMAL, $mockController);
$this->assertEquals('Altered!', $this->router->defaultCallable($matches, '.*$'));
$this->assertEquals('Altered!', $this->router->defaultCallable($matches, $data, '.*$'));
}
/* route() ------------------------------------------------------------ */
@ -401,7 +467,7 @@ class RouterTest extends MVCRTestAbstract
public function testRoute()
{
// Add route first
$this->router->addRoute('(?P<viewName>.*?)(|\/(?P<viewMethod>.*?)(|\/(?P<viewParameters>.*?)))(|\.(?P<viewType>.*?))');
$this->router->addRoute('(?P<viewName>.*?)(|\/(?P<viewMethod>.*?)(|\/(?P<viewParameters>.*?))).test', ['viewType' => 'test']);
// Create mock view and controller
$mockController = $this->getMockBuilder('\FuzeWorks\Controller')->getMock();
@ -418,20 +484,41 @@ class RouterTest extends MVCRTestAbstract
/**
* @depends testRoute
* @covers ::route
* @expectedException \FuzeWorks\Exception\NotFoundException
*/
public function testDistinctRoute()
{
// Create mock view and controller
$mockController = $this->getMockBuilder('\FuzeWorks\Controller')->getMock();
$mockView = $this->getMockBuilder('\FuzeWorks\View')->setMethods(['testMethod'])->getMock();
class_alias(get_class($mockController), '\Application\Controller\TestDistinctRouteController');
class_alias(get_class($mockView), '\Application\View\TestDistinctRouteTestView');
$this->assertNull($this->router->route('testDistinctRoute/testMethod/testParameters.test', [
3 => [
'(?P<viewName>.*?)(|\/(?P<viewMethod>.*?)(|\/(?P<viewParameters>.*?))).test' => ['viewType' => 'test']
]
]));
$this->assertInstanceOf('\Application\Controller\TestDistinctRouteController', $this->router->getCurrentController());
$this->assertInstanceOf('\Application\View\TestDistinctRouteTestView', $this->router->getCurrentView());
}
/**
* @depends testRoute
* @covers ::route
*/
public function testRouteNotFound()
{
$this->expectException(NotFoundException::class);
$this->router->route('NotFound');
}
/**
* @depends testRouteNotFound
* @covers ::route
* @expectedException \FuzeWorks\Exception\NotFoundException
*/
public function testRouteNotMatched()
{
$this->expectException(NotFoundException::class);
$this->router->addRoute('NotMatched');
$this->router->route('NotFound');
}
@ -505,7 +592,7 @@ class RouterTest extends MVCRTestAbstract
public function testRouteCustomCallable()
{
// Create custom callable
$callable = function(array $matches, string $route){
$callable = function(array $matches, array $data, string $route){
$this->assertEquals('customCallable', $route);
$this->assertEquals([0=>'customCallable'], $matches);
};
@ -525,12 +612,11 @@ class RouterTest extends MVCRTestAbstract
* @depends testRouteStaticRewrite
* @covers ::route
* @covers ::loadCallable
* @expectedException \FuzeWorks\Exception\NotFoundException
*/
public function testRouteUnsatisfiedCallable()
{
// Create custom callable
$callable = function(array $matches, string $route){
$callable = function(array $matches, array $data, string $route){
$this->assertEquals('unsatisfiedCallable', $route);
$this->assertEquals([0=>'unsatisfiedCallable'], $matches);
return false;
@ -544,6 +630,7 @@ class RouterTest extends MVCRTestAbstract
]
);
$this->expectException(NotFoundException::class);
$this->assertNull($this->router->route('unsatisfiedCallable'));
}

View File

@ -37,15 +37,19 @@
use FuzeWorks\Controller;
use FuzeWorks\Event\ViewGetEvent;
use FuzeWorks\Events;
use FuzeWorks\Exception\NotFoundException;
use FuzeWorks\Exception\ViewException;
use FuzeWorks\Factory;
use FuzeWorks\Priority;
use FuzeWorks\View;
use FuzeWorks\Views;
use PHPUnit\Framework\TestCase;
/**
* Class ViewsTest
* @coversDefaultClass \FuzeWorks\Views
*/
class ViewsTest extends MVCRTestAbstract
class ViewsTest extends TestCase
{
/**
@ -58,10 +62,10 @@ class ViewsTest extends MVCRTestAbstract
*/
protected $mockController;
public function setUp()
public function setUp(): void
{
$this->views = new Views();
$this->views->addComponentPath('test'.DS.'views');
$this->views->addComponentPath('views');
$this->mockController = $this->getMockBuilder(Controller::class)->getMock();
}
@ -80,11 +84,22 @@ class ViewsTest extends MVCRTestAbstract
$this->assertInstanceOf($mockViewClass, $this->views->get($mockViewClass, $this->mockController, 'Standard', [], '\\'));
}
/**
* Remove all listeners before the next test starts.
*/
public function tearDown(): void
{
// Clear all events created by tests
Events::$listeners = array();
// Reset all config files
Factory::getInstance()->config->discardConfigFiles();
}
/**
* @depends testGetViewFromClass
* @covers ::get
* @covers ::loadView
* @expectedException \FuzeWorks\Exception\ViewException
*/
public function testGetViewFromClassInvalidInstance()
{
@ -94,6 +109,7 @@ class ViewsTest extends MVCRTestAbstract
class_alias($mockFakeViewClass, $mockFakeViewClass . 'StandardView');
// Try and fetch
$this->expectException(ViewException::class);
$this->views->get($mockFakeViewClass, $this->mockController, 'Standard', [], '\\');
}
@ -127,10 +143,10 @@ class ViewsTest extends MVCRTestAbstract
/**
* @covers ::get
* @expectedException \FuzeWorks\Exception\ViewException
*/
public function testGetViewInvalidName()
{
$this->expectException(ViewException::class);
$this->views->get('', $this->mockController, 'Standard', [], '\\');
}
@ -148,10 +164,10 @@ class ViewsTest extends MVCRTestAbstract
* @depends testGetViewFromFile
* @covers ::get
* @covers ::loadView
* @expectedException \FuzeWorks\Exception\ViewException
*/
public function testGetViewFromFileInvalidInstance()
{
$this->expectException(ViewException::class);
$this->views->get('ViewInvalidInstance', $this->mockController);
}
@ -163,8 +179,8 @@ class ViewsTest extends MVCRTestAbstract
public function testDifferentComponentPathPriority()
{
// Add the directories for this test
$this->views->addComponentPath('test'.DS.'views'.DS.'TestDifferentComponentPathPriority'.DS.'Lowest', Priority::LOWEST);
$this->views->addComponentPath('test'.DS.'views'.DS.'TestDifferentComponentPathPriority'.DS.'Highest', Priority::HIGHEST);
$this->views->addComponentPath('views'.DS.'TestDifferentComponentPathPriority'.DS.'Lowest', Priority::LOWEST);
$this->views->addComponentPath('views'.DS.'TestDifferentComponentPathPriority'.DS.'Highest', Priority::HIGHEST);
// Load the view and assert it is the correct type
$view = $this->views->get('TestDifferentComponentPathPriority', $this->mockController);
@ -189,10 +205,10 @@ class ViewsTest extends MVCRTestAbstract
* @depends testGetViewFromFile
* @covers ::get
* @covers ::loadView
* @expectedException \FuzeWorks\Exception\NotFoundException
*/
public function testViewNotFound()
{
$this->expectException(NotFoundException::class);
$this->views->get('NotFound', $this->mockController);
}
@ -200,7 +216,6 @@ class ViewsTest extends MVCRTestAbstract
* @depends testGetViewFromClass
* @covers ::get
* @covers \FuzeWorks\Event\ViewGetEvent::init
* @expectedException \FuzeWorks\Exception\ViewException
*/
public function testViewGetEvent()
{
@ -217,13 +232,13 @@ class ViewsTest extends MVCRTestAbstract
$event->setCancelled(true);
}, 'viewGetEvent', Priority::NORMAL);
$this->expectException(ViewException::class);
$this->views->get('SomeViewName', $this->mockController, 'Other', ['some_path'], 'SomeNamespace', 'Some Argument');
}
/**
* @depends testViewGetEvent
* @covers ::get
* @expectedException \FuzeWorks\Exception\ViewException
*/
public function testCancelGetView()
{
@ -232,6 +247,7 @@ class ViewsTest extends MVCRTestAbstract
$event->setCancelled(true);
}, 'viewGetEvent', Priority::NORMAL);
$this->expectException(ViewException::class);
$this->views->get('SomeView', $this->mockController, 'Standard', [], '\\');
}

View File

@ -1,34 +1,17 @@
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd"
bootstrap="autoload.php"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
colors="false">
<testsuites>
<testsuite name="Core Functionality">
<directory>./</directory>
</testsuite>
</testsuites>
<logging>
<log type="json" target="../build/phpunit/logfile.json"/>
<log type="junit" target="../build/phpunit/logfile.xml"/>
<log type="testdox-html" target="../build/phpunit/testdox.html"/>
<log type="testdox-text" target="../build/phpunit/testdox.txt"/>
</logging>
<filter>
<whitelist processUncoveredFilesFromWhitelist="false">
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" bootstrap="bootstrap.php" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" stopOnError="false" stopOnFailure="false" stopOnIncomplete="false" stopOnSkipped="false" colors="false">
<coverage processUncoveredFiles="false">
<include>
<directory suffix=".php">../</directory>
</include>
<exclude>
<directory suffix=".php">../vendor/</directory>
<directory suffix=".php">../test/</directory>
</exclude>
</whitelist>
</filter>
</coverage>
<testsuites>
<testsuite name="MVCR Suite">
<directory>./</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -29,32 +29,21 @@
* @license https://opensource.org/licenses/MIT MIT License
*
* @link http://techfuze.net/fuzeworks
* @since Version 0.0.1
* @since Version 1.2.0
*
* @version Version 1.2.0
*/
use FuzeWorks\Events;
use PHPUnit\Framework\TestCase;
use FuzeWorks\Factory;
use FuzeWorks\Core;
namespace Custom\View;
/**
* Class MVCRTestAbstract.
*
* Resets core components to their original state
*/
abstract class MVCRTestAbstract extends TestCase
{
/**
* Remove all listeners before the next test starts.
*/
public function tearDown()
{
// Clear all events created by tests
Events::$listeners = array();
use FuzeWorks\View;
// Reset all config files
Factory::getInstance()->config->discardConfigFiles();
class TestDefaultCallableCustomNamespaceTestView extends View
{
public function someMethod()
{
return "Verify Output, with custom namespace!";
}
}