feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
- Base: wintercms/winter branch 1.2 (full framework) - Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS - Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication - Partials: hero (offline-first), features, modes (offline/nube toggle), screenshots, pricing (3 planes), comparison, FAQ, CTA - Plugin VivesPOS.Site with ContactForm - Dockerfile: PHP 8.2 Apache, port 80, healthcheck - Added winter/wn-pages, blog, sitemap, seo plugins - Active theme set to vivespos
This commit is contained in:
221
modules/backend/tests/classes/AuthManagerTest.php
Normal file
221
modules/backend/tests/classes/AuthManagerTest.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace Backend\Tests\Classes;
|
||||
|
||||
use System\Tests\Bootstrap\TestCase;
|
||||
use Winter\Storm\Exception\SystemException;
|
||||
use Backend\Classes\AuthManager;
|
||||
|
||||
class AuthManagerTest extends TestCase
|
||||
{
|
||||
protected AuthManager $instance;
|
||||
protected $existingPermissions = [];
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->createApplication();
|
||||
|
||||
$this->instance = AuthManager::instance();
|
||||
|
||||
$this->existingPermissions = $this->instance->listPermissions();
|
||||
|
||||
$this->instance->registerPermissions('Winter.TestCase', [
|
||||
'test.permission_one' => [
|
||||
'label' => 'Test Permission 1',
|
||||
'tab' => 'Test',
|
||||
'order' => 200
|
||||
],
|
||||
'test.permission_two' => [
|
||||
'label' => 'Test Permission 2',
|
||||
'tab' => 'Test',
|
||||
'order' => 300
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
protected function listNewPermissions()
|
||||
{
|
||||
$existing = collect($this->existingPermissions)->pluck('code')->toArray();
|
||||
$allPermissions = collect($this->instance->listPermissions());
|
||||
|
||||
return $allPermissions->whereNotIn('code', $existing)->pluck('code')->toArray();
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
AuthManager::forgetInstance();
|
||||
}
|
||||
|
||||
public function testListPermissions()
|
||||
{
|
||||
$permissions = $this->listNewPermissions();
|
||||
$this->assertCount(2, $permissions);
|
||||
$this->assertEquals([
|
||||
'test.permission_one',
|
||||
'test.permission_two'
|
||||
], $permissions);
|
||||
}
|
||||
|
||||
public function testRegisterPermissions()
|
||||
{
|
||||
$this->instance->registerPermissions('Winter.TestCase', [
|
||||
'test.permission_three' => [
|
||||
'label' => 'Test Permission 3',
|
||||
'tab' => 'Test',
|
||||
'order' => 100
|
||||
]
|
||||
]);
|
||||
|
||||
$permissions = $this->listNewPermissions();
|
||||
$this->assertCount(3, $permissions);
|
||||
$this->assertEquals([
|
||||
'test.permission_three',
|
||||
'test.permission_one',
|
||||
'test.permission_two'
|
||||
], $permissions);
|
||||
}
|
||||
|
||||
public function testAliasesPermissions()
|
||||
{
|
||||
$this->instance->registerPermissionOwnerAlias('Winter.TestCase', 'Aliased.TestCase');
|
||||
|
||||
$permissions = $this->listNewPermissions();
|
||||
$this->assertCount(2, $permissions);
|
||||
|
||||
$this->instance->removePermission('Aliased.TestCase', 'test.permission_one');
|
||||
|
||||
$permissions = $this->listNewPermissions();
|
||||
$this->assertCount(1, $permissions);
|
||||
$this->assertEquals([
|
||||
'test.permission_two'
|
||||
], $permissions);
|
||||
}
|
||||
|
||||
public function testRegisterPermissionsThroughCallbacks()
|
||||
{
|
||||
// Callback one
|
||||
$this->instance->registerCallback(function ($manager) {
|
||||
$manager->registerPermissions('Winter.TestCase', [
|
||||
'test.permission_three' => [
|
||||
'label' => 'Test Permission 3',
|
||||
'tab' => 'Test',
|
||||
'order' => 100
|
||||
]
|
||||
]);
|
||||
});
|
||||
|
||||
// Callback two
|
||||
$this->instance->registerCallback(function ($manager) {
|
||||
$manager->registerPermissions('Winter.TestCase', [
|
||||
'test.permission_four' => [
|
||||
'label' => 'Test Permission 4',
|
||||
'tab' => 'Test',
|
||||
'order' => 400
|
||||
]
|
||||
]);
|
||||
});
|
||||
|
||||
$permissions = $this->listNewPermissions();
|
||||
$this->assertCount(4, $permissions);
|
||||
$this->assertEquals([
|
||||
'test.permission_three',
|
||||
'test.permission_one',
|
||||
'test.permission_two',
|
||||
'test.permission_four'
|
||||
], $permissions);
|
||||
}
|
||||
|
||||
public function testRegisterAdditionalTab()
|
||||
{
|
||||
$this->instance->registerPermissions('Winter.TestCase', [
|
||||
'test.permission_three' => [
|
||||
'label' => 'Test Permission 3',
|
||||
'tab' => 'Test 2',
|
||||
'order' => 100
|
||||
]
|
||||
]);
|
||||
|
||||
$this->instance->registerCallback(function ($manager) {
|
||||
$manager->registerPermissions('Winter.TestCase', [
|
||||
'test.permission_four' => [
|
||||
'label' => 'Test Permission 4',
|
||||
'tab' => 'Test 2',
|
||||
'order' => 400
|
||||
]
|
||||
]);
|
||||
});
|
||||
|
||||
$tabs = $this->instance->listTabbedPermissions();
|
||||
|
||||
// Remove the core tabs
|
||||
unset($tabs['cms::lang.permissions.name']);
|
||||
unset($tabs['system::lang.permissions.name']);
|
||||
|
||||
$this->assertCount(2, $tabs);
|
||||
$this->assertEquals([
|
||||
'Test 2',
|
||||
'Test'
|
||||
], array_keys($tabs));
|
||||
$this->assertEquals([
|
||||
'test.permission_three',
|
||||
'test.permission_four'
|
||||
], collect($tabs['Test 2'])->pluck('code')->toArray());
|
||||
$this->assertEquals([
|
||||
'test.permission_one',
|
||||
'test.permission_two',
|
||||
], collect($tabs['Test'])->pluck('code')->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions that let their holder change what other backend users see, or
|
||||
* inject markup that renders for them, must warn whoever grants them. The
|
||||
* permission editor surfaces this through the `comment` key.
|
||||
* See GHSA-5cwr-5jxg-pcf6.
|
||||
*/
|
||||
public function testSecuritySensitivePermissionsHaveComments()
|
||||
{
|
||||
$sensitiveCodes = [
|
||||
'backend.manage_users',
|
||||
'backend.impersonate_users',
|
||||
'backend.manage_editor',
|
||||
'backend.manage_branding',
|
||||
'backend.manage_default_dashboard',
|
||||
'backend.allow_unsafe_markdown',
|
||||
];
|
||||
|
||||
$permissions = collect($this->existingPermissions)->keyBy('code');
|
||||
|
||||
foreach ($sensitiveCodes as $code) {
|
||||
$permission = $permissions->get($code);
|
||||
|
||||
$this->assertNotNull($permission, "Permission $code is not registered");
|
||||
$this->assertNotEmpty($permission->comment, "Permission $code is missing a comment");
|
||||
$this->assertNotEquals(
|
||||
$permission->comment,
|
||||
trans($permission->comment),
|
||||
"Permission $code has an unresolved comment language key"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testRemovePermission()
|
||||
{
|
||||
$this->instance->removePermission('Winter.TestCase', 'test.permission_one');
|
||||
|
||||
$permissions = $this->listNewPermissions();
|
||||
$this->assertCount(1, $permissions);
|
||||
$this->assertEquals([
|
||||
'test.permission_two'
|
||||
], $permissions);
|
||||
}
|
||||
|
||||
public function testCannotRemovePermissionsBeforeLoaded()
|
||||
{
|
||||
$this->expectException(SystemException::class);
|
||||
$this->expectExceptionMessage('Unable to remove permissions before they are loaded.');
|
||||
|
||||
AuthManager::forgetInstance();
|
||||
$this->instance = AuthManager::instance();
|
||||
$this->instance->removePermission('Winter.TestCase', 'test.permission_one');
|
||||
}
|
||||
}
|
||||
159
modules/backend/tests/classes/ControllerPostbackTest.php
Normal file
159
modules/backend/tests/classes/ControllerPostbackTest.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace Backend\Tests\Classes;
|
||||
|
||||
use Backend\Controllers\Auth;
|
||||
use Backend\Tests\Fixtures\Models\UserFixture;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use System\Tests\Bootstrap\PluginTestCase;
|
||||
use Winter\Storm\Exception\SystemException;
|
||||
use Winter\Storm\Support\Facades\Config;
|
||||
|
||||
class ControllerPostbackTest extends PluginTestCase
|
||||
{
|
||||
/**
|
||||
* Builds a mock Request that simulates an AJAX POST with the given handler.
|
||||
*/
|
||||
protected function configAjaxRequestMock(string $handler)
|
||||
{
|
||||
$requestMock = $this
|
||||
->getMockBuilder('Illuminate\Http\Request')
|
||||
->disableOriginalConstructor()
|
||||
->setMethods(['ajax', 'method', 'header', 'secure', 'path', 'getScheme', 'getHost', 'getPort', 'getBaseUrl'])
|
||||
->getMock();
|
||||
|
||||
$map = [
|
||||
['X_WINTER_REQUEST_HANDLER', null, $handler],
|
||||
['X_WINTER_REQUEST_PARTIALS', null, ''],
|
||||
['X-CSRF-TOKEN', null, null],
|
||||
['X-XSRF-TOKEN', null, null],
|
||||
];
|
||||
|
||||
$requestMock->expects($this->any())->method('ajax')->willReturn(true);
|
||||
$requestMock->expects($this->any())->method('method')->willReturn('POST');
|
||||
$requestMock->expects($this->any())->method('header')->willReturnMap($map);
|
||||
$requestMock->expects($this->any())->method('secure')->willReturn(false);
|
||||
$requestMock->expects($this->any())->method('path')->willReturn('backend/auth/signin');
|
||||
$requestMock->expects($this->any())->method('getScheme')->willReturn('http');
|
||||
$requestMock->expects($this->any())->method('getHost')->willReturn('localhost');
|
||||
$requestMock->expects($this->any())->method('getPort')->willReturn(80);
|
||||
$requestMock->expects($this->any())->method('getBaseUrl')->willReturn('');
|
||||
|
||||
return $requestMock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a mock Request that simulates a non-AJAX POST with _handler in POST data.
|
||||
*/
|
||||
protected function configPostbackRequestMock(string $handler)
|
||||
{
|
||||
$requestMock = $this
|
||||
->getMockBuilder('Illuminate\Http\Request')
|
||||
->disableOriginalConstructor()
|
||||
->setMethods(['ajax', 'method', 'header', 'post', 'input', 'secure', 'path', 'getScheme', 'getHost', 'getPort', 'getBaseUrl'])
|
||||
->getMock();
|
||||
|
||||
$requestMock->expects($this->any())->method('ajax')->willReturn(false);
|
||||
$requestMock->expects($this->any())->method('method')->willReturn('POST');
|
||||
$requestMock->expects($this->any())->method('header')->willReturn(null);
|
||||
$requestMock->expects($this->any())->method('secure')->willReturn(false);
|
||||
$requestMock->expects($this->any())->method('path')->willReturn('backend/auth/signin');
|
||||
$requestMock->expects($this->any())->method('getScheme')->willReturn('http');
|
||||
$requestMock->expects($this->any())->method('getHost')->willReturn('localhost');
|
||||
$requestMock->expects($this->any())->method('getPort')->willReturn(80);
|
||||
$requestMock->expects($this->any())->method('getBaseUrl')->willReturn('');
|
||||
|
||||
$postData = ['_handler' => $handler];
|
||||
$requestMock->expects($this->any())->method('post')->willReturnCallback(
|
||||
function ($key = null, $default = null) use ($postData) {
|
||||
return $key === null ? $postData : ($postData[$key] ?? $default);
|
||||
}
|
||||
);
|
||||
$requestMock->expects($this->any())->method('input')->willReturnCallback(
|
||||
function ($key = null, $default = null) use ($postData) {
|
||||
return $key === null ? $postData : ($postData[$key] ?? $default);
|
||||
}
|
||||
);
|
||||
|
||||
return $requestMock;
|
||||
}
|
||||
|
||||
//
|
||||
// AJAX header path — validates handler name (existing behavior)
|
||||
//
|
||||
|
||||
public function testAjaxPathRejectsInvalidHandlerName(): void
|
||||
{
|
||||
$this->actingAs((new UserFixture)->asSuperUser());
|
||||
Config::set('cms.enableCsrfProtection', false);
|
||||
|
||||
// Build controller with real Request, then swap for mock before run()
|
||||
$controller = new Auth;
|
||||
Request::swap($this->configAjaxRequestMock('update_onDelete'));
|
||||
|
||||
$this->expectException(SystemException::class);
|
||||
$this->expectExceptionMessage('Invalid AJAX handler name: update_onDelete.');
|
||||
$controller->run('signin');
|
||||
}
|
||||
|
||||
public function testAjaxPathAcceptsValidHandlerName(): void
|
||||
{
|
||||
$this->actingAs((new UserFixture)->asSuperUser());
|
||||
Config::set('cms.enableCsrfProtection', false);
|
||||
|
||||
$controller = new Auth;
|
||||
Request::swap($this->configAjaxRequestMock('onSave'));
|
||||
|
||||
try {
|
||||
$controller->run('signin');
|
||||
} catch (SystemException $e) {
|
||||
// Handler not found is fine — name validation passed
|
||||
$this->assertStringNotContainsString('Invalid AJAX handler name', $e->getMessage());
|
||||
return;
|
||||
}
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Postback _handler path — validates handler name (our fix)
|
||||
//
|
||||
|
||||
public function testPostbackPathRejectsInvalidHandlerName(): void
|
||||
{
|
||||
$this->actingAs((new UserFixture)->asSuperUser());
|
||||
Config::set('cms.enableCsrfProtection', false);
|
||||
|
||||
$controller = new Auth;
|
||||
Request::swap($this->configPostbackRequestMock('update_onDelete'));
|
||||
|
||||
$this->expectException(SystemException::class);
|
||||
$this->expectExceptionMessage('Invalid AJAX handler name: update_onDelete.');
|
||||
$controller->run('signin');
|
||||
}
|
||||
|
||||
public function testPostbackPathRejectsMethodName(): void
|
||||
{
|
||||
$this->actingAs((new UserFixture)->asSuperUser());
|
||||
Config::set('cms.enableCsrfProtection', false);
|
||||
|
||||
$controller = new Auth;
|
||||
Request::swap($this->configPostbackRequestMock('generatePermissionsField'));
|
||||
|
||||
$this->expectException(SystemException::class);
|
||||
$this->expectExceptionMessage('Invalid AJAX handler name: generatePermissionsField.');
|
||||
$controller->run('signin');
|
||||
}
|
||||
|
||||
public function testPostbackPathRejectsActionPrefixedHandler(): void
|
||||
{
|
||||
$this->actingAs((new UserFixture)->asSuperUser());
|
||||
Config::set('cms.enableCsrfProtection', false);
|
||||
|
||||
$controller = new Auth;
|
||||
Request::swap($this->configPostbackRequestMock('create_onSave'));
|
||||
|
||||
$this->expectException(SystemException::class);
|
||||
$this->expectExceptionMessage('Invalid AJAX handler name: create_onSave.');
|
||||
$controller->run('signin');
|
||||
}
|
||||
}
|
||||
494
modules/backend/tests/classes/HandlerDispatchSecurityTest.php
Normal file
494
modules/backend/tests/classes/HandlerDispatchSecurityTest.php
Normal file
@@ -0,0 +1,494 @@
|
||||
<?php
|
||||
|
||||
namespace Backend\Tests\Classes;
|
||||
|
||||
use Backend\Classes\BackendController;
|
||||
use Backend\Tests\Fixtures\Models\UserFixture;
|
||||
use Cms\Classes\Page as CmsPage;
|
||||
use Cms\Classes\Theme as CmsTheme;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use System\Models\EventLog;
|
||||
use System\Models\MailLayout;
|
||||
use System\Tests\Bootstrap\PluginTestCase;
|
||||
use Winter\Storm\Support\Facades\Config;
|
||||
use Winter\Storm\Support\Facades\File;
|
||||
|
||||
/**
|
||||
* Regression coverage for GHSA-p2ch-c2c3-4xm5.
|
||||
*
|
||||
* AJAX handlers (`onFoo`, `index_onFoo`) must not be reachable as backend page actions, in any
|
||||
* spelling, while ordinary page actions and AJAX dispatch keep working.
|
||||
*/
|
||||
class HandlerDispatchSecurityTest extends PluginTestCase
|
||||
{
|
||||
protected $canaryPaths = [];
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Config::set('cms.enableCsrfProtection', true);
|
||||
Config::set('cms.backendUri', 'backend');
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
foreach ($this->canaryPaths as $path) {
|
||||
if (File::exists($path)) {
|
||||
File::delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
EventLog::truncate();
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
protected function parseAction(string $segment): string
|
||||
{
|
||||
$controller = new BackendController();
|
||||
$method = new \ReflectionMethod($controller, 'parseAction');
|
||||
$method->setAccessible(true);
|
||||
|
||||
return $method->invoke($controller, $segment);
|
||||
}
|
||||
|
||||
protected function useTestTheme(): CmsTheme
|
||||
{
|
||||
Config::set('cms.activeTheme', 'test');
|
||||
Config::set('cms.themesPath', '/modules/cms/tests/fixtures/themes');
|
||||
CmsTheme::resetCache();
|
||||
|
||||
return CmsTheme::load('test');
|
||||
}
|
||||
|
||||
protected function seedCanaryPage(CmsTheme $theme, string $fileName): string
|
||||
{
|
||||
$page = CmsPage::inTheme($theme);
|
||||
$page->fileName = $fileName;
|
||||
$page->title = 'CSRF canary';
|
||||
$page->url = '/' . str_replace('.htm', '', $fileName);
|
||||
$page->markup = '<p>canary</p>';
|
||||
$page->save();
|
||||
|
||||
$path = $theme->getPath() . '/pages/' . $fileName;
|
||||
$this->canaryPaths[] = $path;
|
||||
$this->assertTrue(File::exists($path), "Precondition: {$fileName} written to disk");
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
protected function seedEventLog(): void
|
||||
{
|
||||
EventLog::truncate();
|
||||
EventLog::add('csrf canary A');
|
||||
EventLog::add('csrf canary B');
|
||||
}
|
||||
|
||||
protected function canaryCount(): int
|
||||
{
|
||||
return EventLog::where('message', 'like', 'csrf canary%')->count();
|
||||
}
|
||||
|
||||
//
|
||||
// Blocked: handler names must never be reachable as page actions
|
||||
//
|
||||
|
||||
/** The reported primitive, plus its siblings across all three modules. */
|
||||
public function testControllerDeclaredHandlersAreNotReachable()
|
||||
{
|
||||
$cases = [
|
||||
[new \System\Controllers\EventLogs(), 'index_onEmptyLog'],
|
||||
[new \System\Controllers\RequestLogs(), 'index_onEmptyLog'],
|
||||
[new \System\Controllers\MailLayouts(), 'update_onResetDefault'],
|
||||
[new \System\Controllers\MailTemplates(), 'onTest'],
|
||||
[new \System\Controllers\Settings(), 'update_onResetDefault'],
|
||||
[new \Backend\Controllers\Users(), 'update_onUnsuspendUser'],
|
||||
[new \Backend\Controllers\Users(), 'update_onImpersonateUser'],
|
||||
[new \Backend\Controllers\Preferences(), 'index_onResetDefault'],
|
||||
[new \Cms\Controllers\Index(), 'onDelete'],
|
||||
[new \Cms\Controllers\Index(), 'onDeleteTemplates'],
|
||||
[new \Cms\Controllers\Index(), 'onSave'],
|
||||
[new \Cms\Controllers\ThemeOptions(), 'update_onResetDefault'],
|
||||
];
|
||||
|
||||
foreach ($cases as [$controller, $handler]) {
|
||||
$this->assertTrue(
|
||||
$controller->methodExists($handler),
|
||||
get_class($controller) . "::{$handler} must exist for this test to mean anything"
|
||||
);
|
||||
$this->assertFalse(
|
||||
$controller->actionExists($handler),
|
||||
get_class($controller) . "::{$handler} must not be reachable as a page action"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PHP method names are case-insensitive, so both spellings resolve to the same handler --
|
||||
* which is why the guard has to compare the *resolved* name. The lowercase one is the case
|
||||
* that defeats a guard comparing the requested string. Other casings collapse onto these
|
||||
* same two, so they are not repeated.
|
||||
*
|
||||
* The assertTrue() is load-bearing: it proves the spelling really resolves, so that the
|
||||
* assertFalse() beside it cannot pass merely because the method was not found.
|
||||
*/
|
||||
public function testNoCasingOfAHandlerNameIsReachable()
|
||||
{
|
||||
$controller = new \System\Controllers\EventLogs();
|
||||
|
||||
foreach (['index_onEmptyLog', 'index_onemptylog'] as $spelling) {
|
||||
$this->assertTrue(
|
||||
method_exists($controller, $spelling),
|
||||
"Precondition: {$spelling} resolves to the handler"
|
||||
);
|
||||
$this->assertFalse(
|
||||
$controller->actionExists($spelling),
|
||||
"{$spelling} must not be reachable"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* parseAction() lowercases dashed segments, which can turn an arbitrary URL into an
|
||||
* all-lowercase name that still resolves to a mixed-case handler.
|
||||
*/
|
||||
public function testDashedSpellingsCannotLaunderAHandlerName()
|
||||
{
|
||||
$controller = new \System\Controllers\EventLogs();
|
||||
|
||||
// The laundering step is real: this parses to a lowercase name that does resolve.
|
||||
$this->assertEquals('index_onemptylog', $this->parseAction('index-onemptylog'));
|
||||
$this->assertTrue(method_exists($controller, $this->parseAction('index-onemptylog')));
|
||||
|
||||
// One segment per distinct parseAction() result: the laundered name that resolves, and
|
||||
// the two shapes that normalise to something which does not. Extra dash placements all
|
||||
// collapse onto these.
|
||||
foreach (['index-onemptylog', 'index-on-empty-log', 'index_on-emptylog'] as $segment) {
|
||||
$this->assertFalse(
|
||||
$controller->actionExists($this->parseAction($segment)),
|
||||
"Dashed segment '{$segment}' must not reach a handler"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Behaviour handlers. Extension methods are looked up case-sensitively, so only the
|
||||
* canonical spelling resolves at all -- hence the methodExists() assertions below.
|
||||
*/
|
||||
public function testBehaviourProvidedHandlersAreNotReachable()
|
||||
{
|
||||
$users = new \Backend\Controllers\Users();
|
||||
|
||||
// FormController / ListController / RelationController handlers.
|
||||
foreach (['update_onDelete', 'update_onSave', 'create_onSave', 'index_onDelete'] as $handler) {
|
||||
$this->assertTrue($users->methodExists($handler), "Precondition: {$handler} exists");
|
||||
$this->assertFalse($users->actionExists($handler), "{$handler} must not be reachable");
|
||||
}
|
||||
|
||||
// A behaviour that declares no $actions has no allowlist to fall back on, so its
|
||||
// handlers rely entirely on the name guard. Several ecosystem plugins are in this
|
||||
// position, and it is the case most likely to regress.
|
||||
$unguarded = new DispatchProbeBehaviourController();
|
||||
|
||||
$this->assertTrue($unguarded->methodExists('onBar'), 'Precondition: the behaviour supplies it');
|
||||
$this->assertFalse(
|
||||
$unguarded->actionExists('onBar'),
|
||||
'a handler on a behaviour without $actions must still be blocked'
|
||||
);
|
||||
|
||||
foreach (['onbar', 'ONBAR'] as $spelling) {
|
||||
$this->assertFalse(
|
||||
$unguarded->methodExists($spelling),
|
||||
'extension lookup is case-sensitive, so no other casing resolves'
|
||||
);
|
||||
$this->assertFalse($unguarded->actionExists($spelling));
|
||||
}
|
||||
}
|
||||
|
||||
/** Public helpers that were reachable as URLs by accident. */
|
||||
public function testCamelCaseHelpersAreNoLongerRoutable()
|
||||
{
|
||||
$cases = [
|
||||
[new \Backend\Controllers\Files(), 'getThumbUrl'],
|
||||
[new \System\Controllers\Settings(), 'formRender'],
|
||||
[new \System\Controllers\MailBrandSettings(), 'renderSampleMessage'],
|
||||
];
|
||||
|
||||
foreach ($cases as [$controller, $method]) {
|
||||
$this->assertTrue($controller->methodExists($method), "Precondition: {$method} exists");
|
||||
$this->assertFalse($controller->actionExists($method), "{$method} must not be routable");
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Blocked, end to end: token-less GETs must not mutate anything.
|
||||
// One test per way a handler can receive input -- none, a path segment, the query string.
|
||||
//
|
||||
|
||||
public function testTokenlessGetDoesNotTruncateTheEventLog()
|
||||
{
|
||||
$this->seedEventLog();
|
||||
$this->actingAs((new UserFixture)->withPermission('system.access_logs', true));
|
||||
|
||||
foreach (['index_onEmptyLog', 'index_onemptylog'] as $segment) {
|
||||
$status = $this->get("backend/system/eventlogs/{$segment}")->getStatusCode();
|
||||
$this->assertEquals(404, $status, "GET {$segment} must 404");
|
||||
$this->assertEquals(2, $this->canaryCount(), "GET {$segment} must not truncate");
|
||||
}
|
||||
}
|
||||
|
||||
public function testTokenlessGetDoesNotDeleteACmsTemplate()
|
||||
{
|
||||
$theme = $this->useTestTheme();
|
||||
$path = $this->seedCanaryPage($theme, 'csrf-canary.htm');
|
||||
|
||||
$this->actingAs((new UserFixture)->asSuperUser());
|
||||
|
||||
// Cms\Controllers\Index reads its input from Request::input(), which reads the query
|
||||
// string, so the post() helper's method gate does not apply here.
|
||||
$status = $this->get('backend/cms/index/onDelete?' . http_build_query([
|
||||
'theme' => 'test',
|
||||
'templateType' => 'page',
|
||||
'templatePath' => 'csrf-canary.htm',
|
||||
]))->getStatusCode();
|
||||
|
||||
$this->assertEquals(404, $status);
|
||||
$this->assertTrue(File::exists($path), 'A token-less GET must not delete a CMS page');
|
||||
}
|
||||
|
||||
public function testTokenlessGetDoesNotResetAMailLayout()
|
||||
{
|
||||
$layout = MailLayout::first();
|
||||
$this->assertNotNull($layout, 'Precondition: a mail layout exists');
|
||||
|
||||
$layout->content_html = '<p>CUSTOMISED BY OPERATOR</p>';
|
||||
$layout->save();
|
||||
|
||||
$this->actingAs((new UserFixture)->asSuperUser());
|
||||
|
||||
$status = $this->get('backend/system/maillayouts/update_onResetDefault/' . $layout->id)->getStatusCode();
|
||||
|
||||
$this->assertEquals(404, $status);
|
||||
$this->assertEquals(
|
||||
'<p>CUSTOMISED BY OPERATOR</p>',
|
||||
MailLayout::find($layout->id)->content_html,
|
||||
'A token-less GET must not reset a mail layout'
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Still works: nothing legitimate may regress
|
||||
//
|
||||
|
||||
public function testLowercasePageActionsStillResolve()
|
||||
{
|
||||
$users = new \Backend\Controllers\Users();
|
||||
|
||||
// index comes from ListController, create/update/preview from FormController --
|
||||
// all still exposed through each behaviour's $actions allowlist.
|
||||
foreach (['index', 'create', 'update', 'preview'] as $action) {
|
||||
$this->assertTrue($users->actionExists($action), "Page action {$action} must still resolve");
|
||||
}
|
||||
|
||||
$this->assertTrue((new \Cms\Controllers\Index())->actionExists('index'));
|
||||
$this->assertTrue((new \System\Controllers\EventLogs())->actionExists('index'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts dispatch, not rendering: a 404 would mean a valid page action was rejected,
|
||||
* whereas a 500 is unrelated breakage this test should not be hostage to.
|
||||
*/
|
||||
public function testBackendPagesStillDispatch()
|
||||
{
|
||||
$this->actingAs((new UserFixture)->asSuperUser());
|
||||
|
||||
foreach ([
|
||||
'backend/backend/users',
|
||||
'backend/backend/userroles',
|
||||
'backend/backend/usergroups',
|
||||
'backend/system/eventlogs',
|
||||
'backend/system/settings',
|
||||
'backend/system/maillayouts',
|
||||
'backend/backend/myaccount',
|
||||
'backend/backend/preferences',
|
||||
] as $url) {
|
||||
$this->assertNotEquals(
|
||||
404,
|
||||
$this->get($url)->getStatusCode(),
|
||||
"{$url} must still dispatch -- a 404 means the guard rejected a valid page action"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Handlers must stay callable the way the framework actually calls them. */
|
||||
public function testAjaxHandlerDispatchStillWorks()
|
||||
{
|
||||
Config::set('cms.enableCsrfProtection', false);
|
||||
$this->seedEventLog();
|
||||
$this->actingAs((new UserFixture)->asSuperUser());
|
||||
|
||||
$response = $this->post('backend/system/eventlogs', [], [
|
||||
'X-WINTER-REQUEST-HANDLER' => 'onEmptyLog',
|
||||
'X-Requested-With' => 'XMLHttpRequest',
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(0, $this->canaryCount(), 'AJAX dispatch must still reach the handler');
|
||||
}
|
||||
|
||||
/** Dashed URLs keep resolving, now to snake_case rather than camelCase. */
|
||||
public function testDashedUrlsResolveToSnakeCase()
|
||||
{
|
||||
$this->assertEquals('my_action', $this->parseAction('my-action'));
|
||||
$this->assertEquals('index', $this->parseAction('index'));
|
||||
$this->assertEquals('index_onemptylog', $this->parseAction('index-onemptylog'));
|
||||
|
||||
$this->assertTrue((new DispatchProbeSnakeController())->actionExists('coming_soon'));
|
||||
$this->assertTrue((new DispatchProbeFlatController())->actionExists('comingsoon'));
|
||||
$this->assertFalse((new DispatchProbeCamelController())->actionExists('comingSoon'));
|
||||
$this->assertFalse((new DispatchProbeCamelController())->actionExists('comingsoon'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handlers that read post() were always reachable but inert on a GET. They are now
|
||||
* unreachable as well; either way they must not mutate.
|
||||
*/
|
||||
public function testPostGatedHandlersRemainInert()
|
||||
{
|
||||
$this->useTestTheme();
|
||||
$path = themes_path('test');
|
||||
$existedBefore = File::exists($path);
|
||||
|
||||
$this->actingAs((new UserFixture)->asSuperUser());
|
||||
$this->get('backend/cms/themes/index_onDelete?theme=test');
|
||||
|
||||
$this->assertEquals($existedBefore, File::exists($path));
|
||||
}
|
||||
|
||||
//
|
||||
// CMS frontend: structurally immune, pinned so it stays that way
|
||||
//
|
||||
|
||||
/**
|
||||
* The frontend has no page-action dispatch; both handler entry points are POST-gated.
|
||||
* Included here so a change to either gate fails alongside the backend coverage.
|
||||
*/
|
||||
public function testFrontendAjaxHandlerRequiresPost()
|
||||
{
|
||||
$controller = new \Cms\Classes\Controller();
|
||||
|
||||
$headers = [
|
||||
'X-WINTER-REQUEST-HANDLER' => 'onTest',
|
||||
'X-Requested-With' => 'XMLHttpRequest',
|
||||
];
|
||||
|
||||
Request::swap($this->makeRequest('POST', $headers));
|
||||
$this->assertEquals('onTest', $controller->getAjaxHandler(), 'positive control: XHR POST dispatches');
|
||||
|
||||
Request::swap($this->makeRequest('GET', $headers));
|
||||
$this->assertNull($controller->getAjaxHandler(), 'a GET must never yield an AJAX handler');
|
||||
|
||||
Request::swap($this->makeRequest('GET', [], ['_handler' => 'onTest']));
|
||||
$this->assertNull(post('_handler'), 'the _handler postback is unreachable over GET');
|
||||
|
||||
Request::swap($this->makeRequest('POST', [], ['_handler' => 'onTest']));
|
||||
$this->assertEquals('onTest', post('_handler'), 'positive control: POST does supply _handler');
|
||||
}
|
||||
|
||||
protected function makeRequest(string $method, array $headers = [], array $params = [])
|
||||
{
|
||||
$request = \Illuminate\Http\Request::create('/ajax-test', $method, $params);
|
||||
|
||||
foreach ($headers as $key => $value) {
|
||||
$request->headers->set($key, $value);
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
//
|
||||
// Known residual, pinned deliberately
|
||||
//
|
||||
|
||||
/**
|
||||
* Documents a deliberate boundary: an all-lowercase behaviour method stays routable,
|
||||
* because such a name is indistinguishable from an ordinary page action. Declaring
|
||||
* $actions closes it. Tightening this would break legitimate names like onboarding().
|
||||
*/
|
||||
public function testAllLowercaseBehaviourMethodRemainsRoutable()
|
||||
{
|
||||
$this->assertTrue(
|
||||
(new DispatchProbeBehaviourController())->actionExists('onfoo'),
|
||||
'documents the boundary'
|
||||
);
|
||||
$this->assertFalse(
|
||||
(new DispatchProbeBehaviourController())->actionExists('onBar'),
|
||||
'the conventional spelling is still blocked'
|
||||
);
|
||||
$this->assertFalse(
|
||||
(new DispatchProbeGuardedController())->actionExists('onfoo'),
|
||||
'$actions closes it regardless of casing'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Fixtures. Each casing needs its own class: PHP method names are case-insensitive, so
|
||||
// comingSoon() and comingsoon() cannot coexist in one class.
|
||||
//
|
||||
|
||||
class DispatchProbeCamelController extends \Backend\Classes\Controller
|
||||
{
|
||||
public function comingSoon()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class DispatchProbeSnakeController extends \Backend\Classes\Controller
|
||||
{
|
||||
public function coming_soon()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class DispatchProbeFlatController extends \Backend\Classes\Controller
|
||||
{
|
||||
public function comingsoon()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class DispatchProbeBehaviour extends \Backend\Classes\ControllerBehavior
|
||||
{
|
||||
public function onfoo()
|
||||
{
|
||||
}
|
||||
|
||||
public function onBar()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class DispatchProbeGuardedBehaviour extends \Backend\Classes\ControllerBehavior
|
||||
{
|
||||
protected $actions = [];
|
||||
|
||||
public function onfoo()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class DispatchProbeBehaviourController extends \Backend\Classes\Controller
|
||||
{
|
||||
public $implement = [DispatchProbeBehaviour::class];
|
||||
}
|
||||
|
||||
class DispatchProbeGuardedController extends \Backend\Classes\Controller
|
||||
{
|
||||
public $implement = [DispatchProbeGuardedBehaviour::class];
|
||||
}
|
||||
302
modules/backend/tests/classes/NavigationManagerTest.php
Normal file
302
modules/backend/tests/classes/NavigationManagerTest.php
Normal file
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
|
||||
namespace Backend\Tests\Classes;
|
||||
|
||||
use System\Tests\Bootstrap\TestCase;
|
||||
use Backend\Classes\NavigationManager;
|
||||
|
||||
class NavigationManagerTest extends TestCase
|
||||
{
|
||||
public function testRegisterMenuItems()
|
||||
{
|
||||
$manager = NavigationManager::instance();
|
||||
$items = $manager->listMainMenuItems();
|
||||
$this->assertArrayNotHasKey('WINTER.TEST.DASHBOARD', $items);
|
||||
|
||||
$manager->registerMenuItems('Winter.Test', [
|
||||
'dashboard' => [
|
||||
'label' => 'Dashboard',
|
||||
'icon' => 'icon-dashboard',
|
||||
'url' => 'http://example.com',
|
||||
'order' => 100
|
||||
]
|
||||
]);
|
||||
|
||||
$items = $manager->listMainMenuItems();
|
||||
$this->assertArrayHasKey('WINTER.TEST.DASHBOARD', $items);
|
||||
|
||||
$item = $items['WINTER.TEST.DASHBOARD'];
|
||||
$this->assertObjectHasProperty('code', $item);
|
||||
$this->assertObjectHasProperty('label', $item);
|
||||
$this->assertObjectHasProperty('icon', $item);
|
||||
$this->assertObjectHasProperty('url', $item);
|
||||
$this->assertObjectHasProperty('owner', $item);
|
||||
$this->assertObjectHasProperty('order', $item);
|
||||
$this->assertObjectHasProperty('permissions', $item);
|
||||
$this->assertObjectHasProperty('sideMenu', $item);
|
||||
|
||||
$this->assertEquals('dashboard', $item->code);
|
||||
$this->assertEquals('Dashboard', $item->label);
|
||||
$this->assertEquals('icon-dashboard', $item->icon);
|
||||
$this->assertEquals('http://example.com', $item->url);
|
||||
$this->assertEquals(100, $item->order);
|
||||
$this->assertEquals('Winter.Test', $item->owner);
|
||||
}
|
||||
|
||||
public function testListMainMenuItems()
|
||||
{
|
||||
$manager = NavigationManager::instance();
|
||||
$items = $manager->listMainMenuItems();
|
||||
|
||||
$this->assertArrayHasKey('WINTER.TESTER.BLOG', $items);
|
||||
}
|
||||
|
||||
public function testListSideMenuItems()
|
||||
{
|
||||
$manager = NavigationManager::instance();
|
||||
|
||||
$items = $manager->listSideMenuItems();
|
||||
$this->assertEmpty($items);
|
||||
|
||||
$manager->setContext('Winter.Tester', 'blog');
|
||||
|
||||
$items = $manager->listSideMenuItems();
|
||||
$this->assertIsArray($items);
|
||||
$this->assertArrayHasKey('posts', $items);
|
||||
$this->assertArrayHasKey('categories', $items);
|
||||
|
||||
$this->assertIsObject($items['posts']);
|
||||
$this->assertObjectHasProperty('code', $items['posts']);
|
||||
$this->assertObjectHasProperty('owner', $items['posts']);
|
||||
$this->assertEquals('posts', $items['posts']->code);
|
||||
$this->assertEquals('Winter.Tester', $items['posts']->owner);
|
||||
|
||||
$this->assertObjectHasProperty('permissions', $items['posts']);
|
||||
$this->assertIsArray($items['posts']->permissions);
|
||||
$this->assertCount(1, $items['posts']->permissions);
|
||||
|
||||
$this->assertObjectHasProperty('order', $items['posts']);
|
||||
$this->assertObjectHasProperty('order', $items['categories']);
|
||||
$this->assertEquals(100, $items['posts']->order);
|
||||
$this->assertEquals(200, $items['categories']->order);
|
||||
}
|
||||
|
||||
public function testAddMainMenuItems()
|
||||
{
|
||||
$manager = NavigationManager::instance();
|
||||
$manager->addMainMenuItems('Winter.Tester', [
|
||||
'print' => [
|
||||
'label' => 'Print',
|
||||
'icon' => 'icon-print',
|
||||
'url' => 'javascript:window.print()'
|
||||
]
|
||||
]);
|
||||
|
||||
$items = $manager->listMainMenuItems();
|
||||
|
||||
$this->assertIsArray($items);
|
||||
$this->assertArrayHasKey('WINTER.TESTER.PRINT', $items);
|
||||
|
||||
$item = $items['WINTER.TESTER.PRINT'];
|
||||
$this->assertEquals('print', $item->code);
|
||||
$this->assertEquals('Print', $item->label);
|
||||
$this->assertEquals('icon-print', $item->icon);
|
||||
$this->assertEquals('javascript:window.print()', $item->url);
|
||||
$this->assertEquals(500, $item->order);
|
||||
$this->assertEquals('Winter.Tester', $item->owner);
|
||||
}
|
||||
|
||||
public function testAddMainMenuItemsWithAlias()
|
||||
{
|
||||
$manager = NavigationManager::instance();
|
||||
$manager->addMainMenuItems('Winter.Tester', [
|
||||
'print' => [
|
||||
'label' => 'Print',
|
||||
'icon' => 'icon-print',
|
||||
'url' => 'javascript:window.print()'
|
||||
]
|
||||
]);
|
||||
|
||||
$manager->registerOwnerAlias('Winter.Tester', 'Alias.Tester');
|
||||
|
||||
$item = $manager->getMainMenuItem('Alias.Tester', 'print');
|
||||
|
||||
$this->assertEquals('print', $item->code);
|
||||
$this->assertEquals('Print', $item->label);
|
||||
$this->assertEquals('icon-print', $item->icon);
|
||||
$this->assertEquals('javascript:window.print()', $item->url);
|
||||
$this->assertEquals(500, $item->order);
|
||||
$this->assertEquals('Winter.Tester', $item->owner);
|
||||
}
|
||||
|
||||
public function testRemoveMainMenuItem()
|
||||
{
|
||||
$manager = NavigationManager::instance();
|
||||
$manager->addMainMenuItems('Winter.Tester', [
|
||||
'close' => [
|
||||
'label' => 'Close',
|
||||
'icon' => 'icon-times',
|
||||
'url' => 'javascript:window.close()'
|
||||
]
|
||||
]);
|
||||
|
||||
$items = $manager->listMainMenuItems();
|
||||
$this->assertArrayHasKey('WINTER.TESTER.CLOSE', $items);
|
||||
|
||||
$manager->removeMainMenuItem('Winter.Tester', 'close');
|
||||
|
||||
$items = $manager->listMainMenuItems();
|
||||
$this->assertArrayNotHasKey('WINTER.TESTER.CLOSE', $items);
|
||||
}
|
||||
|
||||
public function testRemoveMainMenuItemByAlias()
|
||||
{
|
||||
$manager = NavigationManager::instance();
|
||||
$manager->addMainMenuItems('Winter.Tester', [
|
||||
'close' => [
|
||||
'label' => 'Close',
|
||||
'icon' => 'icon-times',
|
||||
'url' => 'javascript:window.close()'
|
||||
]
|
||||
]);
|
||||
|
||||
$manager->registerOwnerAlias('Winter.Tester', 'Alias.Tester');
|
||||
|
||||
$items = $manager->listMainMenuItems();
|
||||
$this->assertArrayHasKey('WINTER.TESTER.CLOSE', $items);
|
||||
|
||||
$manager->removeMainMenuItem('Alias.Tester', 'close');
|
||||
|
||||
$items = $manager->listMainMenuItems();
|
||||
$this->assertArrayNotHasKey('WINTER.TESTER.CLOSE', $items);
|
||||
}
|
||||
|
||||
public function testAddSideMenuItems()
|
||||
{
|
||||
$manager = NavigationManager::instance();
|
||||
$manager->listMainMenuItems();
|
||||
|
||||
$manager->addSideMenuItems('Winter.Tester', 'blog', [
|
||||
'foo' => [
|
||||
'label' => 'Bar',
|
||||
'icon' => 'icon-derp',
|
||||
'url' => 'http://google.com',
|
||||
'permissions' => [
|
||||
'winter.tester.access_foo',
|
||||
'winter.tester.access_bar'
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$manager->setContext('Winter.Tester', 'blog');
|
||||
$items = $manager->listSideMenuItems();
|
||||
|
||||
$this->assertIsArray($items);
|
||||
$this->assertArrayHasKey('foo', $items);
|
||||
|
||||
$this->assertIsObject($items['foo']);
|
||||
$this->assertObjectHasProperty('code', $items['foo']);
|
||||
$this->assertObjectHasProperty('owner', $items['foo']);
|
||||
$this->assertObjectHasProperty('order', $items['foo']);
|
||||
|
||||
$this->assertEquals(-1, $items['foo']->order);
|
||||
$this->assertEquals('foo', $items['foo']->code);
|
||||
$this->assertEquals('Winter.Tester', $items['foo']->owner);
|
||||
|
||||
$this->assertObjectHasProperty('permissions', $items['foo']);
|
||||
$this->assertIsArray($items['foo']->permissions);
|
||||
$this->assertCount(2, $items['foo']->permissions);
|
||||
$this->assertContains('winter.tester.access_foo', $items['foo']->permissions);
|
||||
$this->assertContains('winter.tester.access_bar', $items['foo']->permissions);
|
||||
}
|
||||
|
||||
public function testAddSideMenuItemsWithAlias()
|
||||
{
|
||||
$manager = NavigationManager::instance();
|
||||
$manager->listMainMenuItems();
|
||||
|
||||
$manager->addSideMenuItems('Winter.Tester', 'blog', [
|
||||
'foo' => [
|
||||
'label' => 'Bar',
|
||||
'icon' => 'icon-derp',
|
||||
'url' => 'http://google.com',
|
||||
'permissions' => [
|
||||
'winter.tester.access_foo',
|
||||
'winter.tester.access_bar'
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$manager->registerOwnerAlias('Winter.Tester', 'Alias.Tester');
|
||||
$manager->setContext('Alias.Tester', 'blog');
|
||||
|
||||
$items = $manager->listSideMenuItems();
|
||||
|
||||
$this->assertTrue(is_array($items));
|
||||
$this->assertArrayHasKey('foo', $items);
|
||||
|
||||
$this->assertTrue(is_object($items['foo']));
|
||||
$this->assertObjectHasProperty('code', $items['foo']);
|
||||
$this->assertObjectHasProperty('owner', $items['foo']);
|
||||
$this->assertObjectHasProperty('order', $items['foo']);
|
||||
|
||||
$this->assertEquals(-1, $items['foo']->order);
|
||||
$this->assertEquals('foo', $items['foo']->code);
|
||||
$this->assertEquals('Winter.Tester', $items['foo']->owner);
|
||||
|
||||
$this->assertObjectHasProperty('permissions', $items['foo']);
|
||||
$this->assertTrue(is_array($items['foo']->permissions));
|
||||
$this->assertCount(2, $items['foo']->permissions);
|
||||
$this->assertContains('winter.tester.access_foo', $items['foo']->permissions);
|
||||
$this->assertContains('winter.tester.access_bar', $items['foo']->permissions);
|
||||
}
|
||||
|
||||
public function testRemoveSideMenuItem()
|
||||
{
|
||||
$manager = NavigationManager::instance();
|
||||
$manager->listMainMenuItems();
|
||||
|
||||
$manager->addSideMenuItems('Winter.Tester', 'blog', [
|
||||
'bar' => [
|
||||
'label' => 'Bar',
|
||||
'icon' => 'icon-bars',
|
||||
'url' => 'http://yahoo.com'
|
||||
]
|
||||
]);
|
||||
|
||||
$manager->setContext('Winter.Tester', 'blog');
|
||||
|
||||
$items = $manager->listSideMenuItems();
|
||||
$this->assertArrayHasKey('bar', $items);
|
||||
|
||||
$manager->removeSideMenuItem('Winter.Tester', 'blog', 'bar');
|
||||
|
||||
$items = $manager->listSideMenuItems();
|
||||
$this->assertArrayNotHasKey('bar', $items);
|
||||
}
|
||||
|
||||
public function testRemoveSideMenuItemByAlias()
|
||||
{
|
||||
$manager = NavigationManager::instance();
|
||||
$manager->listMainMenuItems();
|
||||
|
||||
$manager->addSideMenuItems('Winter.Tester', 'blog', [
|
||||
'bar' => [
|
||||
'label' => 'Bar',
|
||||
'icon' => 'icon-bars',
|
||||
'url' => 'http://yahoo.com'
|
||||
]
|
||||
]);
|
||||
|
||||
$manager->registerOwnerAlias('Winter.Tester', 'Alias.Tester');
|
||||
$manager->setContext('Alias.Tester', 'blog');
|
||||
|
||||
$items = $manager->listSideMenuItems();
|
||||
$this->assertArrayHasKey('bar', $items);
|
||||
|
||||
$manager->removeSideMenuItem('Alias.Tester', 'blog', 'bar');
|
||||
|
||||
$items = $manager->listSideMenuItems();
|
||||
$this->assertArrayNotHasKey('bar', $items);
|
||||
}
|
||||
}
|
||||
49
modules/backend/tests/classes/WidgetManagerTest.php
Normal file
49
modules/backend/tests/classes/WidgetManagerTest.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Backend\Tests\Classes;
|
||||
|
||||
use System\Tests\Bootstrap\TestCase;
|
||||
use Backend\Classes\WidgetManager;
|
||||
|
||||
class WidgetManagerTest extends TestCase
|
||||
{
|
||||
public function testListFormWidgets()
|
||||
{
|
||||
$manager = WidgetManager::instance();
|
||||
$widgets = $manager->listFormWidgets();
|
||||
|
||||
$this->assertArrayHasKey('TestVendor\Test\FormWidgets\Sample', $widgets);
|
||||
$this->assertArrayHasKey('Winter\Tester\FormWidgets\Preview', $widgets);
|
||||
}
|
||||
|
||||
public function testIfWidgetsCanBeExtended()
|
||||
{
|
||||
$manager = WidgetManager::instance();
|
||||
$manager->registerReportWidget('Acme\Fake\ReportWidget\HelloWorld', [
|
||||
'name' => 'Hello World Test',
|
||||
'context' => 'dashboard'
|
||||
]);
|
||||
$widgets = $manager->listReportWidgets();
|
||||
|
||||
$this->assertArrayHasKey('Acme\Fake\ReportWidget\HelloWorld', $widgets);
|
||||
}
|
||||
|
||||
public function testIfWidgetsCanBeRemoved()
|
||||
{
|
||||
$manager = WidgetManager::instance();
|
||||
$manager->registerReportWidget('Acme\Fake\ReportWidget\HelloWorld', [
|
||||
'name' => 'Hello World Test',
|
||||
'context' => 'dashboard'
|
||||
]);
|
||||
$manager->registerReportWidget('Acme\Fake\ReportWidget\ByeWorld', [
|
||||
'name' => 'Hello World Bye',
|
||||
'context' => 'dashboard'
|
||||
]);
|
||||
|
||||
$manager->removeReportWidget('Acme\Fake\ReportWidget\ByeWorld');
|
||||
|
||||
$widgets = $manager->listReportWidgets();
|
||||
|
||||
$this->assertCount(1, $widgets);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user