core: Implement action factory (#1002)

This commit is contained in:
LogMANOriginal 2019-02-06 18:34:51 +01:00 committed by GitHub
parent 69cb65c1af
commit 51ee541d5a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 705 additions and 279 deletions

50
actions/DetectAction.php Normal file
View file

@ -0,0 +1,50 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
class DetectAction extends ActionAbstract {
public function execute() {
$targetURL = $this->userData['url']
or returnClientError('You must specify a url!');
$format = $this->userData['format']
or returnClientError('You must specify a format!');
foreach(Bridge::getBridgeNames() as $bridgeName) {
if(!Bridge::isWhitelisted($bridgeName)) {
continue;
}
$bridge = Bridge::create($bridgeName);
if($bridge === false) {
continue;
}
$bridgeParams = $bridge->detectParameters($targetURL);
if(is_null($bridgeParams)) {
continue;
}
$bridgeParams['bridge'] = $bridgeName;
$bridgeParams['format'] = $format;
header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301);
die();
}
returnClientError('No bridge found for given URL: ' . $targetURL);
}
}

234
actions/DisplayAction.php Normal file
View file

@ -0,0 +1,234 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
class DisplayAction extends ActionAbstract {
public function execute() {
$bridge = array_key_exists('bridge', $this->userData) ? $this->userData['bridge'] : null;
$format = $this->userData['format']
or returnClientError('You must specify a format!');
// DEPRECATED: 'nameFormat' scheme is replaced by 'name' in format parameter values
// this is to keep compatibility until futher complete removal
if(($pos = strpos($format, 'Format')) === (strlen($format) - strlen('Format'))) {
$format = substr($format, 0, $pos);
}
// whitelist control
if(!Bridge::isWhitelisted($bridge)) {
throw new \Exception('This bridge is not whitelisted', 401);
die;
}
// Data retrieval
$bridge = Bridge::create($bridge);
$noproxy = array_key_exists('_noproxy', $this->userData)
&& filter_var($this->userData['_noproxy'], FILTER_VALIDATE_BOOLEAN);
if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
define('NOPROXY', true);
}
// Cache timeout
$cache_timeout = -1;
if(array_key_exists('_cache_timeout', $this->userData)) {
if(!CUSTOM_CACHE_TIMEOUT) {
unset($this->userData['_cache_timeout']);
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($this->userData);
header('Location: ' . $uri, true, 301);
die();
}
$cache_timeout = filter_var($this->userData['_cache_timeout'], FILTER_VALIDATE_INT);
} else {
$cache_timeout = $bridge->getCacheTimeout();
}
// Remove parameters that don't concern bridges
$bridge_params = array_diff_key(
$this->userData,
array_fill_keys(
array(
'action',
'bridge',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
), '')
);
// Remove parameters that don't concern caches
$cache_params = array_diff_key(
$this->userData,
array_fill_keys(
array(
'action',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
), '')
);
// Initialize cache
$cache = Cache::create('FileCache');
$cache->setPath(PATH_CACHE);
$cache->purgeCache(86400); // 24 hours
$cache->setParameters($cache_params);
$items = array();
$infos = array();
$mtime = $cache->getTime();
if($mtime !== false
&& (time() - $cache_timeout < $mtime)
&& !Debug::isEnabled()) { // Load cached data
// Send "Not Modified" response if client supports it
// Implementation based on https://stackoverflow.com/a/10847262
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if($mtime <= $stime) { // Cached data is older or same
header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $mtime) . 'GMT', true, 304);
die();
}
}
$cached = $cache->loadData();
if(isset($cached['items']) && isset($cached['extraInfos'])) {
foreach($cached['items'] as $item) {
$items[] = new \FeedItem($item);
}
$infos = $cached['extraInfos'];
}
} else { // Collect new data
try {
$bridge->setDatas($bridge_params);
$bridge->collectData();
$items = $bridge->getItems();
// Transform "legacy" items to FeedItems if necessary.
// Remove this code when support for "legacy" items ends!
if(isset($items[0]) && is_array($items[0])) {
$feedItems = array();
foreach($items as $item) {
$feedItems[] = new \FeedItem($item);
}
$items = $feedItems;
}
$infos = array(
'name' => $bridge->getName(),
'uri' => $bridge->getURI(),
'icon' => $bridge->getIcon()
);
} catch(Error $e) {
error_log($e);
$item = new \FeedItem();
// Create "new" error message every 24 hours
$this->userData['_error_time'] = urlencode((int)(time() / 86400));
// Error 0 is a special case (i.e. "trying to get property of non-object")
if($e->getCode() === 0) {
$item->setTitle(
'Bridge encountered an unexpected situation! ('
. $this->userData['_error_time']
. ')'
);
} else {
$item->setTitle(
'Bridge returned error '
. $e->getCode()
. '! ('
. $this->userData['_error_time']
. ')'
);
}
$item->setURI(
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?'
. http_build_query($this->userData)
);
$item->setTimestamp(time());
$item->setContent(buildBridgeException($e, $bridge));
$items[] = $item;
} catch(Exception $e) {
error_log($e);
$item = new \FeedItem();
// Create "new" error message every 24 hours
$this->userData['_error_time'] = urlencode((int)(time() / 86400));
$item->setURI(
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?'
. http_build_query($this->userData)
);
$item->setTitle(
'Bridge returned error '
. $e->getCode()
. '! ('
. $this->userData['_error_time']
. ')'
);
$item->setTimestamp(time());
$item->setContent(buildBridgeException($e, $bridge));
$items[] = $item;
}
// Store data in cache
$cache->saveData(array(
'items' => array_map(function($i){ return $i->toArray(); }, $items),
'extraInfos' => $infos
));
}
// Data transformation
try {
$format = Format::create($format);
$format->setItems($items);
$format->setExtraInfos($infos);
$format->setLastModified($cache->getTime());
$format->display();
} catch(Error $e) {
error_log($e);
header('Content-Type: text/html', true, $e->getCode());
die(buildTransformException($e, $bridge));
} catch(Exception $e) {
error_log($e);
header('Content-Type: text/html', true, $e->getCode());
die(buildTransformException($e, $bridge));
}
}
}

53
actions/ListAction.php Normal file
View file

@ -0,0 +1,53 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
class ListAction extends ActionAbstract {
public function execute() {
$list = new StdClass();
$list->bridges = array();
$list->total = 0;
foreach(Bridge::getBridgeNames() as $bridgeName) {
$bridge = Bridge::create($bridgeName);
if($bridge === false) { // Broken bridge, show as inactive
$list->bridges[$bridgeName] = array(
'status' => 'inactive'
);
continue;
}
$status = Bridge::isWhitelisted($bridgeName) ? 'active' : 'inactive';
$list->bridges[$bridgeName] = array(
'status' => $status,
'uri' => $bridge->getURI(),
'name' => $bridge->getName(),
'icon' => $bridge->getIcon(),
'parameters' => $bridge->getParameters(),
'maintainer' => $bridge->getMaintainer(),
'description' => $bridge->getDescription()
);
}
$list->total = count($list->bridges);
header('Content-Type: application/json');
echo json_encode($list, JSON_PRETTY_PRINT);
}
}

286
index.php
View file

@ -51,287 +51,15 @@ $whitelist_default = array(
try { try {
Bridge::setWhitelist($whitelist_default); Bridge::setWhitelist($whitelist_default);
$actionFac = new \ActionFactory();
$actionFac->setWorkingDir(PATH_LIB_ACTIONS);
if(array_key_exists('action', $params)) {
$action = $actionFac->create($params['action']);
$action->setUserData($params);
$action->execute();
} else {
$showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN); $showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN);
$action = array_key_exists('action', $params) ? $params['action'] : null;
$bridge = array_key_exists('bridge', $params) ? $params['bridge'] : null;
// Return list of bridges as JSON formatted text
if($action === 'list') {
$list = new StdClass();
$list->bridges = array();
$list->total = 0;
foreach(Bridge::getBridgeNames() as $bridgeName) {
$bridge = Bridge::create($bridgeName);
if($bridge === false) { // Broken bridge, show as inactive
$list->bridges[$bridgeName] = array(
'status' => 'inactive'
);
continue;
}
$status = Bridge::isWhitelisted($bridgeName) ? 'active' : 'inactive';
$list->bridges[$bridgeName] = array(
'status' => $status,
'uri' => $bridge->getURI(),
'name' => $bridge->getName(),
'icon' => $bridge->getIcon(),
'parameters' => $bridge->getParameters(),
'maintainer' => $bridge->getMaintainer(),
'description' => $bridge->getDescription()
);
}
$list->total = count($list->bridges);
header('Content-Type: application/json');
echo json_encode($list, JSON_PRETTY_PRINT);
} elseif($action === 'detect') {
$targetURL = $params['url']
or returnClientError('You must specify a url!');
$format = $params['format']
or returnClientError('You must specify a format!');
foreach(Bridge::getBridgeNames() as $bridgeName) {
if(!Bridge::isWhitelisted($bridgeName)) {
continue;
}
$bridge = Bridge::create($bridgeName);
if($bridge === false) {
continue;
}
$bridgeParams = $bridge->detectParameters($targetURL);
if(is_null($bridgeParams)) {
continue;
}
$bridgeParams['bridge'] = $bridgeName;
$bridgeParams['format'] = $format;
header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301);
die();
}
returnClientError('No bridge found for given URL: ' . $targetURL);
} elseif($action === 'display' && !empty($bridge)) {
$format = $params['format']
or returnClientError('You must specify a format!');
// DEPRECATED: 'nameFormat' scheme is replaced by 'name' in format parameter values
// this is to keep compatibility until futher complete removal
if(($pos = strpos($format, 'Format')) === (strlen($format) - strlen('Format'))) {
$format = substr($format, 0, $pos);
}
// whitelist control
if(!Bridge::isWhitelisted($bridge)) {
throw new \Exception('This bridge is not whitelisted', 401);
die;
}
// Data retrieval
$bridge = Bridge::create($bridge);
$noproxy = array_key_exists('_noproxy', $params) && filter_var($params['_noproxy'], FILTER_VALIDATE_BOOLEAN);
if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
define('NOPROXY', true);
}
// Cache timeout
$cache_timeout = -1;
if(array_key_exists('_cache_timeout', $params)) {
if(!CUSTOM_CACHE_TIMEOUT) {
unset($params['_cache_timeout']);
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($params);
header('Location: ' . $uri, true, 301);
die();
}
$cache_timeout = filter_var($params['_cache_timeout'], FILTER_VALIDATE_INT);
} else {
$cache_timeout = $bridge->getCacheTimeout();
}
// Remove parameters that don't concern bridges
$bridge_params = array_diff_key(
$params,
array_fill_keys(
array(
'action',
'bridge',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
), '')
);
// Remove parameters that don't concern caches
$cache_params = array_diff_key(
$params,
array_fill_keys(
array(
'action',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
), '')
);
// Initialize cache
$cache = Cache::create('FileCache');
$cache->setPath(PATH_CACHE);
$cache->purgeCache(86400); // 24 hours
$cache->setParameters($cache_params);
$items = array();
$infos = array();
$mtime = $cache->getTime();
if($mtime !== false
&& (time() - $cache_timeout < $mtime)
&& !Debug::isEnabled()) { // Load cached data
// Send "Not Modified" response if client supports it
// Implementation based on https://stackoverflow.com/a/10847262
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if($mtime <= $stime) { // Cached data is older or same
header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $mtime) . 'GMT', true, 304);
die();
}
}
$cached = $cache->loadData();
if(isset($cached['items']) && isset($cached['extraInfos'])) {
foreach($cached['items'] as $item) {
$items[] = new \FeedItem($item);
}
$infos = $cached['extraInfos'];
}
} else { // Collect new data
try {
$bridge->setDatas($bridge_params);
$bridge->collectData();
$items = $bridge->getItems();
// Transform "legacy" items to FeedItems if necessary.
// Remove this code when support for "legacy" items ends!
if(isset($items[0]) && is_array($items[0])) {
$feedItems = array();
foreach($items as $item) {
$feedItems[] = new \FeedItem($item);
}
$items = $feedItems;
}
$infos = array(
'name' => $bridge->getName(),
'uri' => $bridge->getURI(),
'icon' => $bridge->getIcon()
);
} catch(Error $e) {
error_log($e);
$item = new \FeedItem();
// Create "new" error message every 24 hours
$params['_error_time'] = urlencode((int)(time() / 86400));
// Error 0 is a special case (i.e. "trying to get property of non-object")
if($e->getCode() === 0) {
$item->setTitle('Bridge encountered an unexpected situation! (' . $params['_error_time'] . ')');
} else {
$item->setTitle('Bridge returned error ' . $e->getCode() . '! (' . $params['_error_time'] . ')');
}
$item->setURI(
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?'
. http_build_query($params)
);
$item->setTimestamp(time());
$item->setContent(buildBridgeException($e, $bridge));
$items[] = $item;
} catch(Exception $e) {
error_log($e);
$item = new \FeedItem();
// Create "new" error message every 24 hours
$params['_error_time'] = urlencode((int)(time() / 86400));
$item->setURI(
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?'
. http_build_query($params)
);
$item->setTitle('Bridge returned error ' . $e->getCode() . '! (' . $params['_error_time'] . ')');
$item->setTimestamp(time());
$item->setContent(buildBridgeException($e, $bridge));
$items[] = $item;
}
// Store data in cache
$cache->saveData(array(
'items' => array_map(function($i){ return $i->toArray(); }, $items),
'extraInfos' => $infos
));
}
// Data transformation
try {
$format = Format::create($format);
$format->setItems($items);
$format->setExtraInfos($infos);
$format->setLastModified($cache->getTime());
$format->display();
} catch(Error $e) {
error_log($e);
header('Content-Type: text/html', true, $e->getCode());
die(buildTransformException($e, $bridge));
} catch(Exception $e) {
error_log($e);
header('Content-Type: text/html', true, $e->getCode());
die(buildTransformException($e, $bridge));
}
} else {
echo BridgeList::create($showInactive); echo BridgeList::create($showInactive);
} }
} catch(\Exception $e) { } catch(\Exception $e) {

33
lib/ActionAbstract.php Normal file
View file

@ -0,0 +1,33 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* An abstract class for action objects
*/
abstract class ActionAbstract implements ActionInterface {
/**
* Holds the user data.
*
* @var array
*/
protected $userData = null;
/**
* {@inheritdoc}
*
* @param array $userData {@inheritdoc}
*/
public function setUserData($userData) {
$this->userData = $userData;
}
}

65
lib/ActionFactory.php Normal file
View file

@ -0,0 +1,65 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Factory for action objects.
*/
class ActionFactory extends FactoryAbstract {
/**
* {@inheritdoc}
*
* @param string $name {@inheritdoc}
*/
public function create($name) {
$filePath = $this->buildFilePath($name);
if(!file_exists($filePath)) {
throw new \Exception('File ' . $filePath . ' does not exist!');
}
require_once $filePath;
$class = $this->buildClassName($name);
if((new \ReflectionClass($class))->isInstantiable()) {
return new $class();
}
return false;
}
/**
* Build class name from action name
*
* The class name consists of the action name with prefix "Action". The first
* character of the class name must be uppercase.
*
* Example: 'display' => 'DisplayAction'
*
* @param string $name The action name.
* @return string The class name.
*/
protected function buildClassName($name) {
return ucfirst(strtolower($name)) . 'Action';
}
/**
* Build file path to the action class.
*
* @param string $name The action name.
* @return string Path to the action class.
*/
protected function buildFilePath($name) {
return $this->getWorkingDir() . $this->buildClassName($name) . '.php';
}
}

34
lib/ActionInterface.php Normal file
View file

@ -0,0 +1,34 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Interface for action objects.
*/
interface ActionInterface {
/**
* Set user data for the action to consume.
*
* @param array $userData An associative array of user data.
* @return void
*/
function setUserData($userData);
/**
* Execute the action.
*
* Note: This function directly outputs data to the user.
*
* @return void
*/
function execute();
}

70
lib/FactoryAbstract.php Normal file
View file

@ -0,0 +1,70 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Abstract class for factories.
*/
abstract class FactoryAbstract {
/**
* Holds the working directory
*
* @var string
*/
private $workingDir = null;
/**
* Set the working directory.
*
* @param string $dir The working directory.
* @return void
*/
public function setWorkingDir($dir) {
$this->workingDir = null;
if(!is_string($dir)) {
throw new \InvalidArgumentException('Working directory must be a string!');
}
if(!file_exists($dir)) {
throw new \Exception('Working directory does not exist!');
}
if(!is_dir($dir)) {
throw new \InvalidArgumentException($dir . ' is not a directory!');
}
$this->workingDir = realpath($dir) . '/';
}
/**
* Get the working directory
*
* @return string The working directory.
*/
public function getWorkingDir() {
if(is_null($this->workingDir)) {
throw new \LogicException('Working directory is not set!');
}
return $this->workingDir;
}
/**
* Creates a new instance for the object specified by name.
*
* @param string $name The name of the object to create.
* @return object The object instance
*/
abstract public function create($name);
}

View file

@ -29,6 +29,9 @@ define('PATH_LIB_FORMATS', __DIR__ . '/../formats/');
/** Path to the caches library */ /** Path to the caches library */
define('PATH_LIB_CACHES', __DIR__ . '/../caches/'); define('PATH_LIB_CACHES', __DIR__ . '/../caches/');
/** Path to the actions library */
define('PATH_LIB_ACTIONS', __DIR__ . '/../actions/');
/** Path to the cache folder */ /** Path to the cache folder */
define('PATH_CACHE', __DIR__ . '/../cache/'); define('PATH_CACHE', __DIR__ . '/../cache/');
@ -39,11 +42,13 @@ define('WHITELIST', __DIR__ . '/../whitelist.txt');
define('REPOSITORY', 'https://github.com/RSS-Bridge/rss-bridge/'); define('REPOSITORY', 'https://github.com/RSS-Bridge/rss-bridge/');
// Interfaces // Interfaces
require_once PATH_LIB . 'ActionInterface.php';
require_once PATH_LIB . 'BridgeInterface.php'; require_once PATH_LIB . 'BridgeInterface.php';
require_once PATH_LIB . 'CacheInterface.php'; require_once PATH_LIB . 'CacheInterface.php';
require_once PATH_LIB . 'FormatInterface.php'; require_once PATH_LIB . 'FormatInterface.php';
// Classes // Classes
require_once PATH_LIB . 'FactoryAbstract.php';
require_once PATH_LIB . 'FeedItem.php'; require_once PATH_LIB . 'FeedItem.php';
require_once PATH_LIB . 'Debug.php'; require_once PATH_LIB . 'Debug.php';
require_once PATH_LIB . 'Exceptions.php'; require_once PATH_LIB . 'Exceptions.php';
@ -58,6 +63,8 @@ require_once PATH_LIB . 'Configuration.php';
require_once PATH_LIB . 'BridgeCard.php'; require_once PATH_LIB . 'BridgeCard.php';
require_once PATH_LIB . 'BridgeList.php'; require_once PATH_LIB . 'BridgeList.php';
require_once PATH_LIB . 'ParameterValidator.php'; require_once PATH_LIB . 'ParameterValidator.php';
require_once PATH_LIB . 'ActionFactory.php';
require_once PATH_LIB . 'ActionAbstract.php';
// Functions // Functions
require_once PATH_LIB . 'html.php'; require_once PATH_LIB . 'html.php';

View file

@ -14,6 +14,9 @@
<testsuite name="formats"> <testsuite name="formats">
<directory suffix="FormatTest.php">tests</directory> <directory suffix="FormatTest.php">tests</directory>
</testsuite> </testsuite>
<testsuite name="actions">
<directory suffix="ActionTest.php">tests</directory>
</testsuite>
</testsuites> </testsuites>
</phpunit> </phpunit>

View file

@ -0,0 +1,59 @@
<?php
require_once __DIR__ . '/../lib/rssbridge.php';
use PHPUnit\Framework\TestCase;
class ActionImplementationTest extends TestCase {
private $class;
private $obj;
/**
* @dataProvider dataActionsProvider
*/
public function testClassName($path) {
$this->setAction($path);
$this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
$this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
$this->assertStringEndsWith('Action', $this->class, 'class name must end with "Action"');
}
/**
* @dataProvider dataActionsProvider
*/
public function testClassType($path) {
$this->setAction($path);
$this->assertInstanceOf(ActionInterface::class, $this->obj);
}
/**
* @dataProvider dataActionsProvider
*/
public function testVisibleMethods($path) {
$allowedActionAbstract = get_class_methods(ActionAbstract::class);
sort($allowedActionAbstract);
$this->setAction($path);
$methods = get_class_methods($this->obj);
sort($methods);
$this->assertEquals($allowedActionAbstract, $methods);
}
////////////////////////////////////////////////////////////////////////////
public function dataActionsProvider() {
$actions = array();
foreach (glob(PATH_LIB_ACTIONS . '*.php') as $path) {
$actions[basename($path, '.php')] = array($path);
}
return $actions;
}
private function setAction($path) {
require_once $path;
$this->class = basename($path, '.php');
$this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
$this->obj = new $this->class();
}
}

90
tests/ListActionTest.php Normal file
View file

@ -0,0 +1,90 @@
<?php
require_once __DIR__ . '/../lib/rssbridge.php';
use PHPUnit\Framework\TestCase;
class ListActionTest extends TestCase {
private $action;
private $data;
/**
* @runInSeparateProcess
* @requires function xdebug_get_headers
*/
public function testHeaders() {
$this->initAction();
$this->assertContains(
'Content-Type: application/json',
xdebug_get_headers()
);
}
/**
* @runInSeparateProcess
*/
public function testOutput() {
$this->initAction();
$items = json_decode($this->data, true);
$this->assertNotNull($items, 'invalid JSON output: ' . json_last_error_msg());
$this->assertArrayHasKey('total', $items, 'Missing "total" parameter');
$this->assertInternalType('int', $items['total'], 'Invalid type');
$this->assertArrayHasKey('bridges', $items, 'Missing "bridges" array');
$this->assertEquals(
$items['total'],
count($items['bridges']),
'Item count doesn\'t match'
);
$this->assertEquals(
count(Bridge::getBridgeNames()),
count($items['bridges']),
'Number of bridges doesn\'t match'
);
$expectedKeys = array(
'status',
'uri',
'name',
'icon',
'parameters',
'maintainer',
'description'
);
$allowedStatus = array(
'active',
'inactive'
);
foreach($items['bridges'] as $bridge) {
foreach($expectedKeys as $key) {
$this->assertArrayHasKey($key, $bridge, 'Missing key "' . $key . '"');
}
$this->assertContains($bridge['status'], $allowedStatus, 'Invalid status value');
}
}
////////////////////////////////////////////////////////////////////////////
private function initAction() {
$actionFac = new ActionFactory();
$actionFac->setWorkingDir(PATH_LIB_ACTIONS);
$this->action = $actionFac->create('list');
$this->action->setUserData(array()); /* no user data required */
ob_start();
$this->action->execute();
$this->data = ob_get_contents();
ob_clean();
ob_end_flush();
}
}