Files
Cloud-CMS/tests/LocalWebTestCase.php
Matt Batchelder 05ce0da296 Initial Upload
2025-12-02 10:32:59 -05:00

571 lines
19 KiB
PHP

<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Tests;
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Monolog\Processor\UidProcessor;
use Nyholm\Psr7\ServerRequest;
use PHPUnit\Framework\TestCase as PHPUnit_TestCase;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Slim\App;
use Slim\Http\ServerRequest as Request;
use Slim\Views\TwigMiddleware;
use Xibo\Entity\Application;
use Xibo\Entity\User;
use Xibo\Factory\ContainerFactory;
use Xibo\Factory\TaskFactory;
use Xibo\Middleware\State;
use Xibo\Middleware\Storage;
use Xibo\OAuth2\Client\Provider\XiboEntityProvider;
use Xibo\Service\DisplayNotifyService;
use Xibo\Service\ReportService;
use Xibo\Storage\PdoStorageService;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Tests\Helper\MockPlayerActionService;
use Xibo\Tests\Middleware\TestAuthMiddleware;
use Xibo\Tests\Xmds\XmdsWrapper;
use Xibo\XTR\TaskInterface;
/**
* Class LocalWebTestCase
* @package Xibo\Tests
*/
class LocalWebTestCase extends PHPUnit_TestCase
{
/** @var ContainerInterface */
public static $container;
/** @var LoggerInterface */
public static $logger;
/** @var TaskInterface */
public static $taskService;
/** @var XiboEntityProvider */
public static $entityProvider;
/** @var XmdsWrapper */
public static $xmds;
/** @var App */
protected $app;
/**
* Get Entity Provider
* @return XiboEntityProvider
*/
public function getEntityProvider()
{
return self::$entityProvider;
}
/**
* Get Xmds Wrapper
* @return XmdsWrapper
*/
public function getXmdsWrapper()
{
return self::$xmds;
}
/**
* Gets the Slim instance configured
* @return App
* @throws \Exception
*/
public function getSlimInstance()
{
// Create the container for dependency injection.
try {
$container = ContainerFactory::create();
} catch (\Exception $e) {
die($e->getMessage());
}
// Create a Slim application
$app = \DI\Bridge\Slim\Bridge::create($container);
$twigMiddleware = TwigMiddleware::createFromContainer($app);
// Create a logger
$handlers = [];
if (isset($_SERVER['PHPUNIT_LOG_TO_FILE']) && $_SERVER['PHPUNIT_LOG_TO_FILE']) {
$handlers[] = new StreamHandler(PROJECT_ROOT . '/library/log.txt', Logger::DEBUG);
}
if (isset($_SERVER['PHPUNIT_LOG_WEB_TO_CONSOLE']) && $_SERVER['PHPUNIT_LOG_WEB_TO_CONSOLE']) {
$handlers[] = new StreamHandler(STDERR, Logger::DEBUG);
}
if (count($handlers) <= 0) {
$handlers[] = new NullHandler();
}
$container->set('logger', function (ContainerInterface $container) use ($handlers) {
$logger = new Logger('PHPUNIT');
$uidProcessor = new UidProcessor();
$logger->pushProcessor($uidProcessor);
foreach ($handlers as $handler) {
$logger->pushHandler($handler);
}
return $logger;
});
// Config
$container->set('name', 'test');
$container->get('configService');
// Set app state
\Xibo\Middleware\State::setState($app, $this->createRequest('GET', '/'));
// Setting Middleware
$app->add(new \Xibo\Middleware\ListenersMiddleware($app));
$app->add(new TestAuthMiddleware($app));
$app->add(new State($app));
$app->add($twigMiddleware);
$app->add(new Storage($app));
$app->add(new Middleware\TestXmr($app));
$app->addRoutingMiddleware();
// Add Error Middleware
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorMiddleware->setDefaultErrorHandler(\Xibo\Middleware\Handlers::testErrorHandler($container));
// All routes
require PROJECT_ROOT . '/lib/routes-web.php';
require PROJECT_ROOT . '/lib/routes.php';
// Add the route for running a task manually
$app->get('/tasks/{id}', ['\Xibo\Controller\Task','run']);
return $app;
}
/**
* @param string $method
* @param string $path
* @param array $headers
* @param string $requestAttrVal
* @param bool|false $ajaxHeader
* @param null $body
* @return ResponseInterface
*/
protected function sendRequest(string $method, string $path, $body = null, array $headers = ['HTTP_ACCEPT'=>'application/json'], $requestAttrVal = 'test', $ajaxHeader = false): ResponseInterface
{
// Create a request for tests
$request = new Request(new ServerRequest($method, $path, $headers));
$request = $request->withAttribute('_entryPoint', $requestAttrVal);
// If we are using POST or PUT method then we expect to have Body provided, add it to the request
if (in_array($method, ['POST', 'PUT']) && $body != null) {
$request = $request->withParsedBody($body);
// in case we forgot to set Content-Type header for PUT requests
if ($method === 'PUT') {
$request = $request->withHeader('Content-Type', 'application/x-www-form-urlencoded');
}
}
if ($ajaxHeader === true) {
$request = $request->withHeader('X-Requested-With', 'XMLHttpRequest');
}
if ($method == 'GET' && $body != null) {
$request = $request->withQueryParams($body);
}
// send the request and return the response
return $this->app->handle($request);
}
/**
* @param string $method
* @param string $path
* @param null $body
* @param array $headers
* @param array $serverParams
* @return Request
*/
protected function createRequest(string $method, string $path, $body = null, array $headers = ['HTTP_ACCEPT'=>'application/json'], $serverParams = []): Request
{
// Create a request for tests
$request = new Request(new ServerRequest($method, $path, $headers, $body, '', $serverParams));
$request = $request->withAttribute('_entryPoint', 'test');
return $request;
}
/**
* Create a global container for all tests to share.
* @throws \Exception
*/
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
// Configure global test state
// We want to ensure there is a
// - global DB object
// - phpunit user who executes the tests through Slim
// - an API application owned by phpunit with client_credentials grant type
if (self::$container == null) {
self::getLogger()->debug('Creating Container');
// Create a new container
$container = ContainerFactory::create();
// Create a logger
$handlers = [];
if (isset($_SERVER['PHPUNIT_LOG_TO_FILE']) && $_SERVER['PHPUNIT_LOG_TO_FILE']) {
$handlers[] = new StreamHandler(PROJECT_ROOT . '/library/log.txt', Logger::INFO);
} else {
$handlers[] = new NullHandler();
}
if (isset($_SERVER['PHPUNIT_LOG_CONTAINER_TO_CONSOLE']) && $_SERVER['PHPUNIT_LOG_CONTAINER_TO_CONSOLE']) {
$handlers[] = new StreamHandler(STDERR, Logger::DEBUG);
}
$container->set('logger', function (ContainerInterface $container) use ($handlers) {
$logger = new Logger('PHPUNIT');
$uidProcessor = new UidProcessor();
$logger->pushProcessor($uidProcessor);
foreach ($handlers as $handler) {
$logger->pushHandler($handler);
}
return $logger;
});
// Initialise config
$container->get('configService');
$container->get('configService')->setDependencies($container->get('store'), '/');
// This is our helper container.
$container->set('name', 'phpunit');
// Configure the container with Player Action and Display Notify
// Player Action Helper
$container->set('playerActionService', function (ContainerInterface $c) {
return new MockPlayerActionService(
$c->get('configService'),
$c->get('logService'),
false
);
});
// Register the display notify service
$container->set('displayNotifyService', function (ContainerInterface $c) {
return new DisplayNotifyService(
$c->get('configService'),
$c->get('logService'),
$c->get('store'),
$c->get('pool'),
$c->get('playerActionService'),
$c->get('scheduleFactory')
);
});
// Register the report service
$container->set('reportService', function (ContainerInterface $c) {
return new ReportService(
$c,
$c->get('store'),
$c->get('timeSeriesStore'),
$c->get('logService'),
$c->get('configService'),
$c->get('sanitizerService'),
$c->get('savedReportFactory')
);
});
// <editor-fold desc="Create PHPUnit container users">
// Find the PHPUnit user and if we don't create it
try {
/** @var User $user */
$user = $container->get('userFactory')->getByName('phpunit');
$user->setChildAclDependencies($container->get('userGroupFactory'));
// Load the user
$user->load(false);
} catch (NotFoundException $e) {
// Create the phpunit user with a random password
/** @var \Xibo\Entity\User $user */
$user = $container->get('userFactory')->create();
$user->setChildAclDependencies($container->get('userGroupFactory'));
$user->userTypeId = 1;
$user->userName = 'phpunit';
$user->libraryQuota = 0;
$user->homePageId = 'statusdashboard.view';
$user->homeFolderId = 1;
$user->isSystemNotification = 1;
$user->setNewPassword(\Xibo\Helper\Random::generateString());
$user->save();
$container->get('store')->commitIfNecessary();
}
// Set on the container
$container->set('user', $user);
// Find the phpunit user and if we don't, complain
try {
/** @var User $admin */
$admin = $container->get('userFactory')->getByName('phpunit');
} catch (NotFoundException $e) {
die ('Cant proceed without the phpunit user');
}
// Check to see if there is an API application we can use
try {
/** @var Application $application */
$application = $container->get('applicationFactory')->getByName('phpunit');
} catch (NotFoundException $e) {
// Add it
$application = $container->get('applicationFactory')->create();
$application->name = ('phpunit');
$application->authCode = 0;
$application->clientCredentials = 1;
$application->userId = $admin->userId;
$application->assignScope($container->get('applicationScopeFactory')->getById('all'));
$application->save();
/** @var PdoStorageService $store */
$store = $container->get('store');
$store->commitIfNecessary();
}
//</editor-fold>
// Register a provider and entity provider to act as our API wrapper
$provider = new \Xibo\OAuth2\Client\Provider\Xibo([
'clientId' => $application->key,
'clientSecret' => $application->secret,
'redirectUri' => null,
'baseUrl' => 'http://localhost'
]);
// Discover the CMS key for XMDS
/** @var PdoStorageService $store */
$store = $container->get('store');
$key = $store->select('SELECT value FROM `setting` WHERE `setting` = \'SERVER_KEY\'', [])[0]['value'];
$store->commitIfNecessary();
// Create an XMDS wrapper for the tests to use
$xmds = new XmdsWrapper('http://localhost/xmds.php', $key);
// Store our entityProvider
self::$entityProvider = new XiboEntityProvider($provider);
// Store our XmdsWrapper
self::$xmds = $xmds;
// Store our container
self::$container = $container;
}
}
/**
* Convenience function to skip a test with a reason and close output buffers nicely.
* @param string $reason
*/
public function skipTest($reason)
{
$this->markTestSkipped($reason);
}
/**
* Get Store
* @return StorageServiceInterface
*/
public function getStore()
{
return self::$container->get('store');
}
/**
* Get a task object
* @param string $task The path of the task class
* @return TaskInterface
* @throws NotFoundException
*/
public function getTask($task)
{
$c = self::$container;
/** @var TaskFactory $taskFactory */
$taskFactory = $c->get('taskFactory');
$task = $taskFactory->getByClass($task);
/** @var TaskInterface $taskClass */
$taskClass = new $task->class();
return $taskClass
->setSanitizer($c->get('sanitizerService'))
->setUser($c->get('user'))
->setConfig($c->get('configService'))
->setLogger($c->get('logService'))
->setPool($c->get('pool'))
->setStore($c->get('store'))
->setTimeSeriesStore($c->get('timeSeriesStore'))
->setDispatcher($c->get('dispatcher'))
->setFactories($c)
->setTask($task);
}
/**
* @return LoggerInterface
* @throws \Exception
*/
public static function getLogger()
{
// Create if necessary
if (self::$logger === null) {
if (isset($_SERVER['PHPUNIT_LOG_TO_CONSOLE']) && $_SERVER['PHPUNIT_LOG_TO_CONSOLE']) {
self::$logger = new Logger('TESTS', [new StreamHandler(STDERR, Logger::DEBUG)]);
} else {
self::$logger = new NullLogger();
}
}
return self::$logger;
}
/**
* Get the queue of actions.
* @return int[]
*/
public function getPlayerActionQueue()
{
/** @var \Xibo\Service\PlayerActionServiceInterface $service */
$service = $this->app->getContainer()->get('playerActionService');
if ($service === null) {
$this->fail('Test has not used the client and therefore cannot determine XMR activity');
}
return $service->getQueue();
}
/**
* @param $name
* @param $class
*/
protected static function installModuleIfNecessary($name, $class)
{
// Make sure the HLS widget is installed
$res = self::$container->get('store')->select('SELECT * FROM `module` WHERE `module` = :module', ['module' => $name]);
if (count($res) <= 0) {
// Install the module
self::$container->get('store')->insert('
INSERT INTO `module` (`Module`, `Name`, `Enabled`, `RegionSpecific`, `Description`,
`SchemaVersion`, `ValidExtensions`, `PreviewEnabled`, `assignable`, `render_as`, `settings`, `viewPath`, `class`, `defaultDuration`, `installName`)
VALUES (:module, :name, :enabled, :region_specific, :description,
:schema_version, :valid_extensions, :preview_enabled, :assignable, :render_as, :settings, :viewPath, :class, :defaultDuration, :installName)
', [
'module' => $name,
'name' => $name,
'enabled' => 1,
'region_specific' => 1,
'description' => $name,
'schema_version' => 1,
'valid_extensions' => null,
'preview_enabled' => 1,
'assignable' => 1,
'render_as' => 'html',
'settings' => json_encode([]),
'viewPath' => '../modules',
'class' => $class,
'defaultDuration' => 10,
'installName' => $name
]);
self::$container->get('store')->commitIfNecessary();
}
}
/**
* @inheritDoc
* @throws \Exception
*/
public function setUp(): void
{
self::getLogger()->debug('LocalWebTestCase: setUp');
parent::setUp();
// Establish a local reference to the Slim app object
$this->app = $this->getSlimInstance();
}
/**
* @inheritDoc
* @throws \Exception
*/
public function tearDown(): void
{
self::getLogger()->debug('LocalWebTestCase: tearDown');
// Close and tidy up the app
$this->app->getContainer()->get('store')->close();
$this->app = null;
parent::tearDown();
}
/**
* Set the _SERVER vars for the suite
* @param array $userSettings
*/
public static function setEnvironment($userSettings = [])
{
$defaults = [
'REQUEST_METHOD' => 'GET',
'REQUEST_URI' => '/',
'SCRIPT_NAME' => '',
'PATH_INFO' => '/',
'QUERY_STRING' => '',
'SERVER_NAME' => 'local.dev',
'SERVER_PORT' => 80,
'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.8',
'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
'USER_AGENT' => 'Slim Framework',
'REMOTE_ADDR' => '127.0.0.1',
];
$environmentSettings = array_merge($userSettings, $defaults);
foreach ($environmentSettings as $key => $value) {
$_SERVER[$key] = $value;
}
}
}