feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
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:
2026-08-21 19:29:00 -06:00
commit 1f72193a64
3266 changed files with 531480 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
<?php
namespace Cms\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\Asset;
use Cms\Classes\Theme;
class AssetTest extends TestCase
{
public function testLoad()
{
$theme = Theme::load('test');
// Valid direct path
$this->assertStringContainsString(
'console.log(\'script1.js\');',
Asset::load($theme, 'js/script1.js')->content
);
// Valid direct subdirectory path
$this->assertStringContainsString(
'console.log(\'subdir/script1.js\');',
Asset::load($theme, 'js/subdir/script1.js')->content
);
// Valid relative path
$this->assertStringContainsString(
'console.log(\'script2.js\');',
Asset::load($theme, 'js/subdir/../script2.js')->content
);
// Invalid theme path
$this->assertNull(
Asset::load($theme, 'js/invalid.js')
);
// Check that we cannot break out of assets directory
$this->assertNull(
Asset::load($theme, '../../../../js/helpers/fakeDom.js')
);
$this->assertNull(
Asset::load($theme, '../content/html-content.htm')
);
// Check that we cannot load directories directly
$this->assertNull(
Asset::load($theme, 'js/subdir')
);
// Check that we definitely cannot load external PHP files
$this->assertNull(
Asset::load($theme, '../../../../../config/database.php')
);
}
public function testGetPath()
{
// Test some pathing fringe cases
$theme = Theme::load('test');
$assetClass = new Asset($theme);
$themeDir = $theme->getPath();
// Direct paths
$this->assertEquals(
str_replace('/', DIRECTORY_SEPARATOR, $themeDir . '/assets/js/script1.js'),
$assetClass->getFilePath('js/script1.js')
);
$this->assertEquals(
str_replace('/', DIRECTORY_SEPARATOR, $themeDir . '/assets/js/script1.js'),
$assetClass->getFilePath('/js/script1.js')
);
// Direct path to a directory
$this->assertEquals(
str_replace('/', DIRECTORY_SEPARATOR, $themeDir . '/assets/js/subdir'),
$assetClass->getFilePath('/js/subdir')
);
$this->assertEquals(
str_replace('/', DIRECTORY_SEPARATOR, $themeDir . '/assets/js/subdir'),
$assetClass->getFilePath('/js/subdir/')
);
// Relative paths
$this->assertEquals(
str_replace('/', DIRECTORY_SEPARATOR, $themeDir . '/assets/js/script2.js'),
$assetClass->getFilePath('./js/script2.js')
);
$this->assertEquals(
str_replace('/', DIRECTORY_SEPARATOR, $themeDir . '/assets/js/script2.js'),
$assetClass->getFilePath('/js/subdir/../script2.js')
);
// Missing file, but valid directory (allows for new files)
$this->assertEquals(
str_replace('/', DIRECTORY_SEPARATOR, $themeDir . '/assets/js/missing.js'),
$assetClass->getFilePath('/js/missing.js')
);
$this->assertEquals(
str_replace('/', DIRECTORY_SEPARATOR, $themeDir . '/assets/js/missing.js'),
$assetClass->getFilePath('js/missing.js')
);
// Missing file and missing directory (new directories are created as needed)
$this->assertEquals(
str_replace('/', DIRECTORY_SEPARATOR, $themeDir . '/assets/js/missing/missing.js'),
$assetClass->getFilePath('/js/missing/missing.js')
);
// Ensure we cannot get paths outside of the assets directory
$this->assertFalse(
$assetClass->getFilePath('../../../../js/helpers/fakeDom.js')
);
$this->assertFalse(
$assetClass->getFilePath('../content/html-content.htm')
);
$this->assertFalse(
$assetClass->getFilePath('../../../../../config/database.php')
);
}
}

View File

@@ -0,0 +1,217 @@
<?php
namespace Cms\Tests\Classes;
use Exception;
use System\Tests\Bootstrap\PluginTestCase;
use Cms\Classes\AutoDatasource;
use Winter\Storm\Database\MemoryCache;
use Winter\Storm\Database\Model;
use Winter\Storm\Halcyon\Datasource\DbDatasource;
use Winter\Storm\Halcyon\Datasource\FileDatasource;
use Winter\Storm\Support\Facades\DB;
class CmsThemeTemplateFixture extends Model
{
protected $guarded = [];
public $timestamps = false;
public $table = 'cms_theme_templates';
}
class AutoDatasourceTest extends PluginTestCase
{
/**
* Array of model fixtures.
*
* @var array
*/
public $fixtures = [];
/**
* AutoDatasource object.
*
* @var Cms\Classes\AutoDatasource;
*/
public $datasource;
public function setUp(): void
{
parent::setUp();
$this->fixtures = [];
// Create fixtures of template data
$this->fixtures[] = CmsThemeTemplateFixture::create([
'source' => 'test',
'path' => 'partials/page-partial.htm',
'content' => 'AutoDatasource partials/page-partial.htm',
'file_size' => 40
]);
$this->fixtures[] = CmsThemeTemplateFixture::create([
'source' => 'test',
'path' => 'partials/testpost/default.htm',
'content' => 'AutoDatasource partials/testpost/default.htm',
'file_size' => 44
]);
$this->fixtures[] = CmsThemeTemplateFixture::create([
'source' => 'test',
'path' => 'partials/subdir/test.htm',
'content' => 'AutoDatasource partials/subdir/test.htm',
'file_size' => 39,
'updated_at' => '2019-06-01 12:00:00'
]);
$this->fixtures[] = CmsThemeTemplateFixture::create([
'source' => 'test',
'path' => 'partials/nesting/level2.htm',
'content' => 'AutoDatasource partials/nesting/level2.htm',
'file_size' => 42,
'deleted_at' => '2019-01-01 00:00:00'
]);
// Create AutoDatasource
$this->datasource = new AutoDatasource([
'database' => new DbDatasource('test', 'cms_theme_templates'),
'filesystem' => new FileDatasource(
base_path('modules/system/tests/fixtures/themes/test'),
\App::make('files')
),
]);
}
public function tearDown(): void
{
foreach ($this->fixtures as $fixture) {
$fixture->delete();
}
parent::tearDown();
}
public function testSelect()
{
$results = collect($this->datasource->select('partials'))
->keyBy('fileName')
->toArray();
// Should be 14 partials in filesystem (tests/fixtures/themes/test), and 1 created directly in database.
// 1 of the filesystem partials should be marked deleted in database.
$this->assertCount(14, $results);
// Database-only partial should be available
$this->assertArrayHasKey('subdir/test.htm', $results);
$this->assertEquals(
'AutoDatasource partials/subdir/test.htm',
$results['subdir/test.htm']['content']
);
// Two filesystem partials should be overriden by database
$this->assertEquals(
'AutoDatasource partials/page-partial.htm',
$results['page-partial.htm']['content']
);
$this->assertEquals(
'AutoDatasource partials/testpost/default.htm',
$results['testpost/default.htm']['content']
);
// One filesystem partial should be marked deleted in database
$this->assertArrayNotHasKey('nesting/level2.htm', $results);
}
public function testPathCacheValueShapes()
{
$pathCache = self::getProtectedProperty($this->datasource, 'pathCache');
// Database records report their last modified time, deleted records report false
$this->assertIsInt($pathCache[0]['partials/subdir/test.htm']);
$this->assertEquals(
strtotime('2019-06-01 12:00:00'),
$pathCache[0]['partials/subdir/test.htm']
);
$this->assertFalse($pathCache[0]['partials/nesting/level2.htm']);
// Filesystem records continue to report true so that their mtime is resolved live
$this->assertTrue($pathCache[1]['partials/layout-partial.htm']);
}
public function testLastModifiedIsServedFromPathCacheWithoutQuerying()
{
// The duplicate query cache would otherwise mask a query issued by this call
MemoryCache::instance()->flush();
DB::connection()->flushQueryLog();
DB::connection()->enableQueryLog();
$mtime = $this->datasource->lastModified('partials', 'subdir/test', 'htm');
$queries = DB::connection()->getQueryLog();
DB::connection()->disableQueryLog();
$this->assertEquals(strtotime('2019-06-01 12:00:00'), $mtime);
$this->assertCount(0, $queries, 'lastModified() should not query the database');
}
public function testLastModifiedFallsBackToFilesystemDatasource()
{
$path = base_path('modules/system/tests/fixtures/themes/test/partials/layout-partial.htm');
$this->assertEquals(
filemtime($path),
$this->datasource->lastModified('partials', 'layout-partial', 'htm')
);
}
public function testLastModifiedIsStableForNullUpdatedAt()
{
$this->fixtures[] = CmsThemeTemplateFixture::create([
'source' => 'test',
'path' => 'partials/no-timestamp.htm',
'content' => 'AutoDatasource partials/no-timestamp.htm',
'file_size' => 40,
'updated_at' => null,
]);
$this->datasource->populateCache(true);
$pathCache = self::getProtectedProperty($this->datasource, 'pathCache');
$cached = $pathCache[0]['partials/no-timestamp.htm'];
// updated_at is nullable, and Carbon::parse(null) resolves to "now". The value is
// resolved once, when the path cache is built, so lastModified() reports it
// consistently rather than returning a different result on every call.
// Note this does not make the Halcyon cache usable for such records: selectOne()
// still resolves their mtime live, so the two disagree and the cache is busted on
// every request. That is pre-existing and not addressed here.
$this->assertIsInt($cached);
$this->assertEquals($cached, $this->datasource->lastModified('partials', 'no-timestamp', 'htm'));
}
public function testLastModifiedThrowsForDeletedPath()
{
$this->expectException(Exception::class);
$this->expectExceptionMessage('partials/nesting/level2.htm is deleted');
$this->datasource->lastModified('partials', 'nesting/level2', 'htm');
}
public function testLastModifiedReflectsUpdatesMadeThroughTheDatasource()
{
$before = $this->datasource->lastModified('partials', 'subdir/test', 'htm');
$this->datasource->update('partials', 'subdir/test', 'htm', 'Updated content');
$after = $this->datasource->lastModified('partials', 'subdir/test', 'htm');
// Editing a template must take effect immediately, without clearing the cache
$this->assertGreaterThan($before, $after);
$this->assertEquals(
'Updated content',
$this->datasource->selectOne('partials', 'subdir/test', 'htm')['content']
);
}
}

View File

@@ -0,0 +1,333 @@
<?php
namespace Cms\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\CmsCompoundObject;
use Cms\Classes\CmsObject;
use Cms\Classes\Theme;
use Winter\Storm\Halcyon\Model;
class TestCmsCompoundObject extends CmsCompoundObject
{
protected $dirName = 'testobjects';
protected function parseSettings()
{
}
}
class TestParsedCmsCompoundObject extends CmsCompoundObject
{
protected $dirName = 'testobjects';
}
class TestTemporaryCmsCompoundObject extends CmsCompoundObject
{
protected $dirName = 'temporary';
protected function parseSettings()
{
}
}
class CmsCompoundObjectTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
Model::clearBootedModels();
Model::flushEventListeners();
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/Archive.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/Post.php';
}
public function testLoadFile()
{
$theme = Theme::load('test');
$obj = TestCmsCompoundObject::load($theme, 'compound.htm');
$this->assertStringContainsString("\$controller->data['something'] = 'some value'", $obj->code);
$this->assertEquals('<p>This is a paragraph</p>', $obj->markup);
$this->assertIsArray($obj->settings);
$this->assertArrayHasKey('var', $obj->settings);
$this->assertEquals('value', $obj->settings['var']);
$this->assertArrayHasKey('components', $obj->settings);
$this->assertArrayHasKey('section', $obj->settings['components']);
$this->assertIsArray($obj->settings['components']['section']);
$this->assertArrayHasKey('version', $obj->settings['components']['section']);
$this->assertEquals(10, $obj->settings['components']['section']['version']);
$this->assertEquals('value', $obj->var);
$this->assertArrayHasKey('version', $obj->settings['components']['section']);
$this->assertEquals(10, $obj->settings['components']['section']['version']);
}
public function testParseComponentSettings()
{
$theme = Theme::load('test');
$obj = TestCmsCompoundObject::load($theme, 'component.htm');
$this->assertArrayHasKey('components', $obj->settings);
$this->assertIsArray($obj->settings['components']);
$this->assertArrayHasKey('testArchive', $obj->settings['components']);
$this->assertArrayHasKey('posts-per-page', $obj->settings['components']['testArchive']);
$this->assertEquals(10, $obj->settings['components']['testArchive']['posts-per-page']);
}
public function testHasComponent()
{
$theme = Theme::load('test');
$obj = TestCmsCompoundObject::load($theme, 'components.htm');
$this->assertArrayHasKey('components', $obj->settings);
$this->assertIsArray($obj->settings['components']);
$this->assertArrayHasKey('testArchive firstAlias', $obj->settings['components']);
$this->assertArrayHasKey('Winter\Tester\Components\Post secondAlias', $obj->settings['components']);
// Explicit
$this->assertEquals('testArchive firstAlias', $obj->hasComponent('testArchive'));
$this->assertEquals('Winter\Tester\Components\Post secondAlias', $obj->hasComponent('Winter\Tester\Components\Post'));
// Resolved
$this->assertEquals('testArchive firstAlias', $obj->hasComponent('Winter\Tester\Components\Archive'));
$this->assertEquals('Winter\Tester\Components\Post secondAlias', $obj->hasComponent('testPost'));
// Negative test
$this->assertFalse($obj->hasComponent('yooHooBigSummerBlowOut'));
$this->assertFalse($obj->hasComponent('Winter\Tester\Components\BigSummer'));
}
public function testGetComponentProperties()
{
$theme = Theme::load('test');
$obj = TestCmsCompoundObject::load($theme, 'components.htm');
$properties = $obj->getComponentProperties('Winter\Tester\Components\Post');
$emptyProperties = $obj->getComponentProperties('Winter\Tester\Components\Archive');
$notExistingProperties = $obj->getComponentProperties('This\Is\Not\Component');
$this->assertIsArray($properties);
$this->assertArrayHasKey('show-featured', $properties);
$this->assertTrue((bool)$properties['show-featured']);
$this->assertEquals('true', $properties['show-featured']);
$this->assertCount(1, $properties);
$this->assertCount(0, $emptyProperties);
$this->assertCount(0, $notExistingProperties);
}
public function testCache()
{
$theme = Theme::load('test');
$themePath = $theme->getPath();
/*
* Prepare the test file
*/
$srcPath = $themePath . '/testobjects/compound.htm';
$this->assertFileExists($srcPath);
$testContent = file_get_contents($srcPath);
$this->assertNotEmpty($testContent);
$filePath = $themePath .= '/temporary/testcompound.htm';
if (file_exists($filePath)) {
@unlink($filePath);
}
$this->assertFileNotExists($filePath);
file_put_contents($filePath, $testContent);
/*
* Load the test object to initialize the cache
*/
$obj = TestTemporaryCmsCompoundObject::loadCached($theme, 'testcompound.htm');
$this->assertFalse($obj->isLoadedFromCache());
$this->assertEquals($testContent, $obj->getContent());
$this->assertEquals('testcompound.htm', $obj->getFileName());
$this->assertEquals('<p>This is a paragraph</p>', $obj->markup);
$this->assertIsArray($obj->settings);
$this->assertArrayHasKey('var', $obj->settings);
$this->assertEquals('value', $obj->settings['var']);
$this->assertArrayHasKey('components', $obj->settings);
$this->assertIsArray($obj->settings['components']['section']);
$this->assertArrayHasKey('version', $obj->settings['components']['section']);
$this->assertEquals(10, $obj->settings['components']['section']['version']);
$this->assertEquals('value', $obj->var);
$this->assertIsArray($obj->settings['components']['section']);
$this->assertArrayHasKey('version', $obj->settings['components']['section']);
$this->assertEquals(10, $obj->settings['components']['section']['version']);
/*
* Load the test object again, it should be loaded from the cache this time
*/
CmsObject::clearInternalCache();
$obj = TestTemporaryCmsCompoundObject::loadCached($theme, 'testcompound.htm');
$this->assertTrue($obj->isLoadedFromCache());
$this->assertEquals($testContent, $obj->getContent());
$this->assertEquals('testcompound.htm', $obj->getFileName());
$this->assertEquals('<p>This is a paragraph</p>', $obj->markup);
$this->assertIsArray($obj->settings);
$this->assertArrayHasKey('var', $obj->settings);
$this->assertEquals('value', $obj->settings['var']);
$this->assertArrayHasKey('components', $obj->settings);
$this->assertIsArray($obj->settings['components']['section']);
$this->assertArrayHasKey('version', $obj->settings['components']['section']);
$this->assertEquals(10, $obj->settings['components']['section']['version']);
$this->assertEquals('value', $obj->var);
$this->assertIsArray($obj->settings['components']['section']);
$this->assertArrayHasKey('version', $obj->settings['components']['section']);
$this->assertEquals(10, $obj->settings['components']['section']['version']);
}
public function testUndefinedProperty()
{
$obj = new TestCmsCompoundObject;
$this->assertNull($obj->something);
}
public function testSaveMarkup()
{
$theme = Theme::load('apitest');
$destFilePath = $theme->getPath() . '/testobjects/compound-markup.htm';
if (file_exists($destFilePath)) {
unlink($destFilePath);
}
$this->assertFileNotExists($destFilePath);
$obj = TestCmsCompoundObject::inTheme($theme);
$obj->fill([
'markup' => '<p>Hello, world!</p>',
'fileName' => 'compound-markup'
]);
$obj->save();
$referenceFilePath = base_path() . '/modules/cms/tests/fixtures/reference/compound-markup.htm';
$this->assertFileExists($referenceFilePath);
$this->assertFileExists($destFilePath);
$this->assertFileEqualsNormalized($referenceFilePath, $destFilePath);
}
public function testSaveMarkupAndSettings()
{
$theme = Theme::load('apitest');
$destFilePath = $theme->getPath() . '/testobjects/compound-markup-settings.htm';
if (file_exists($destFilePath)) {
unlink($destFilePath);
}
$this->assertFileNotExists($destFilePath);
$obj = TestCmsCompoundObject::inTheme($theme);
$obj->fill([
'settings' => ['var' => 'value'],
'markup' => '<p>Hello, world!</p>',
'fileName' => 'compound-markup-settings'
]);
$obj->save();
$referenceFilePath = base_path() . '/modules/cms/tests/fixtures/reference/compound-markup-settings.htm';
$this->assertFileExists($referenceFilePath);
$this->assertFileExists($destFilePath);
$this->assertFileEqualsNormalized($referenceFilePath, $destFilePath);
}
public function testSaveFull()
{
$theme = Theme::load('apitest');
$destFilePath = $theme->getPath() . '/testobjects/compound.htm';
if (file_exists($destFilePath)) {
unlink($destFilePath);
}
$this->assertFileNotExists($destFilePath);
$obj = TestCmsCompoundObject::inTheme($theme);
$obj->fill([
'fileName' => 'compound',
'settings' => ['var' => 'value'],
'code' => 'function a() {return true;}',
'markup' => '<p>Hello, world!</p>'
]);
$obj->save();
$referenceFilePath = base_path() . '/modules/cms/tests/fixtures/reference/compound-full.htm';
$this->assertFileExists($referenceFilePath);
$this->assertFileExists($destFilePath);
$this->assertFileEqualsNormalized($referenceFilePath, $destFilePath);
}
public function testGetViewBagPopulated()
{
$theme = Theme::load('test');
$obj = TestParsedCmsCompoundObject::load($theme, 'viewbag.htm');
$this->assertNull($obj->code);
$this->assertEquals('<p>Chop Suey!</p>', $obj->markup);
$this->assertIsArray($obj->settings);
$this->assertArrayHasKey('var', $obj->settings);
$this->assertEquals('value', $obj->settings['var']);
$this->assertArrayHasKey('components', $obj->settings);
$this->assertArrayHasKey('viewBag', $obj->settings['components']);
$this->assertIsArray($obj->settings['components']['viewBag']);
$this->assertArrayHasKey('title', $obj->settings['components']['viewBag']);
$this->assertEquals('Toxicity', $obj->settings['components']['viewBag']['title']);
$viewBag = $obj->getViewBag();
$properties = $viewBag->getProperties();
$this->assertCount(1, $properties);
$this->assertEquals($obj->viewBag, $properties);
$this->assertInstanceOf('Cms\Components\ViewBag', $viewBag);
$this->assertArrayHasKey('title', $properties);
$this->assertEquals('Toxicity', $properties['title']);
}
public function testGetViewBagEmpty()
{
$theme = Theme::load('test');
$obj = TestParsedCmsCompoundObject::load($theme, 'compound.htm');
$viewBag = $obj->getViewBag();
$this->assertInstanceOf('Cms\Components\ViewBag', $viewBag);
$properties = $viewBag->getProperties();
$this->assertEmpty($properties);
$this->assertEquals($obj->viewBag, $properties);
}
//
// Helpers
//
protected function assertFileEqualsNormalized($expected, $actual)
{
$expected = file_get_contents($expected);
$expected = preg_replace('~\R~u', PHP_EOL, $expected); // Normalize EOL
$actual = file_get_contents($actual);
$actual = preg_replace('~\R~u', PHP_EOL, $actual); // Normalize EOL
$this->assertEquals($expected, $actual);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Cms\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\CmsException;
use Cms\Classes\Router;
use Cms\Classes\Theme;
use Winter\Storm\Exception\SystemException;
class CmsExceptionTest extends TestCase
{
//
// Tests
//
public function testExceptionMask()
{
$foreignException = new \Exception('This is a general error');
$exceptionMask = new SystemException('This is a system exception');
$exceptionMask->setMask($foreignException);
$this->assertEquals('This is a general error', $exceptionMask->getMessage());
}
public function testCmsExceptionPhp()
{
$theme = Theme::load('test');
$router = new Router($theme);
$page = $router->findByUrl('/throw-php');
$error = [
'file' => 'test.php',
'line' => 20,
];
$foreignException = new \Symfony\Component\ErrorHandler\Error\FatalError('This is a general error', 100, $error);
$this->setProtectedProperty($foreignException, 'file', "/modules/cms/classes/CodeParser.php(165) : eval()'d code line 7");
$exception = new CmsException($page, 300);
$exception->setMask($foreignException);
$this->assertEquals($page->getFilePath(), $exception->getFile());
$this->assertEquals('PHP Content', $exception->getErrorType());
$this->assertEquals('This is a general error', $exception->getMessage());
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Cms\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\Layout;
use Cms\Classes\Page;
use Winter\Storm\Halcyon\Model;
class CmsObjectQueryTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
Model::clearBootedModels();
Model::flushEventListeners();
}
public function testWhere()
{
$page = Page::where('layout', 'caramba')->first();
$this->assertEquals('/no-layout', $page->url);
}
public function testWhereComponent()
{
$pages = Page::whereComponent('testArchive', 'posts-per-page', '6');
$this->assertCount(1, $pages->all());
$page = $pages->first();
$this->assertEquals('/with-components', $page->url);
}
public function testWithComponent()
{
$pages = Page::withComponent('testArchive')->all();
$this->assertCount(2, $pages);
foreach ($pages as $page) {
$this->assertTrue(!!$page->hasComponent('testArchive'));
}
}
public function testWithComponentCallback()
{
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/Archive.php';
$pages = Page::withComponent('testArchive', function ($component) {
return $component->property('posts-per-page') == '69';
})->all();
$this->assertCount(1, $pages);
}
public function testLists()
{
// Default theme: test
$pages = Page::lists('baseFileName');
sort($pages);
$this->assertEquals([
"404",
"a/a-page",
"ajax-test",
"authors",
"b/b-page",
"blog-archive",
"blog-category",
"blog-post",
"code-namespaces",
"code-namespaces-aliases",
"component-custom-render",
"component-partial",
"component-partial-alias-override",
"component-partial-nesting",
"component-partial-override",
"cycle-test",
"filters-test",
"index",
"no-component",
"no-component-class",
"no-layout",
"no-partial",
"no-soft-component-class",
"optional-full-php-tags",
"optional-short-php-tags",
"shared-variable",
"shared-variable-override",
"throw-php",
"with-component",
"with-components",
"with-content",
"with-layout",
"with-macro",
"with-partials",
"with-placeholder",
"with-soft-component-class",
"with-soft-component-class-alias",
], $pages);
$layouts = Layout::lists('baseFileName');
sort($layouts);
$this->assertEquals([
"a/a-layout",
"ajax-test",
"content",
"cycle-test",
"no-php",
"partials",
"php-parser-test",
"placeholder",
"sidebar",
], $layouts);
}
public function testListsNonExistentTheme()
{
$pages = Page::inTheme('NON_EXISTENT_THEME')->lists('baseFileName');
$this->assertEmpty($pages);
}
}

View File

@@ -0,0 +1,343 @@
<?php
namespace Cms\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\CmsObject;
use Cms\Classes\Theme;
class TestCmsObject extends CmsObject
{
protected $dirName = 'testobjects';
protected $allowedExtensions = ['htm', 'html'];
}
class TestTemporaryCmsObject extends CmsObject
{
protected $dirName = 'temporary';
}
class CmsObjectTest extends TestCase
{
public function testLoad()
{
$theme = Theme::load('test');
$obj = TestCmsObject::load($theme, 'plain.html');
$this->assertEquals('<p>This is a test HTML content file.</p>', $obj->getContent());
$this->assertEquals('plain.html', $obj->getFileName());
$path = str_replace('/', DIRECTORY_SEPARATOR, $theme->getPath() . '/testobjects/plain.html');
$this->assertEquals($path, $obj->getFilePath());
$this->assertEquals(filemtime($path), $obj->mtime);
}
public function testLoadFromSubdirectory()
{
$theme = Theme::load('test');
$obj = TestCmsObject::load($theme, 'subdir/obj.html');
$this->assertEquals('<p>This is an object in a subdirectory.</p>', $obj->getContent());
$this->assertEquals('subdir/obj.html', $obj->getFileName());
$path = str_replace('/', DIRECTORY_SEPARATOR, $theme->getPath() . '/testobjects/subdir/obj.html');
$this->assertEquals($path, $obj->getFilePath());
$this->assertEquals(filemtime($path), $obj->mtime);
}
public function testValidateLoadInvalidTheme()
{
$theme = Theme::load('none');
$this->assertNull(TestCmsObject::load($theme, 'plain.html'));
}
public function testValidateLoadInvalidFile()
{
$theme = Theme::load('test');
$this->assertNull(TestCmsObject::load($theme, 'none'));
}
public function testCache()
{
$theme = Theme::load('test');
$themePath = $theme->getPath();
$filePath = $themePath .= '/temporary/test.htm';
if (file_exists($filePath)) {
@unlink($filePath);
}
$this->assertFileNotExists($filePath);
file_put_contents($filePath, '<p>Test content</p>');
/*
* First try - the object should be loaded from the file
*/
$obj = TestTemporaryCmsObject::loadCached($theme, 'test.htm');
$this->assertFalse($obj->isLoadedFromCache());
$this->assertEquals('<p>Test content</p>', $obj->getContent());
$this->assertEquals('test.htm', $obj->getFileName());
$this->assertEquals(filemtime($filePath), $obj->mtime);
/*
* Second try - the object should be loaded from the cache
*/
CmsObject::clearInternalCache();
$obj = TestTemporaryCmsObject::loadCached($theme, 'test.htm');
$this->assertTrue($obj->isLoadedFromCache());
$this->assertEquals('<p>Test content</p>', $obj->getContent());
$this->assertEquals('test.htm', $obj->getFileName());
$this->assertEquals(filemtime($filePath), $obj->mtime);
/*
* Modify the file. The object should be loaded from the disk and re-cached.
*/
sleep(1); // Sleep a second in order to have the update file modification time
file_put_contents($filePath, '<p>Updated test content</p>');
clearstatcache(); // The filemtime() function caches its value within a request, so we should clear its cache.
CmsObject::clearInternalCache();
$obj = TestTemporaryCmsObject::loadCached($theme, 'test.htm');
$this->assertFalse($obj->isLoadedFromCache());
$this->assertEquals('<p>Updated test content</p>', $obj->getContent());
$this->assertEquals(filemtime($filePath), $obj->mtime);
CmsObject::clearInternalCache();
$obj = TestTemporaryCmsObject::loadCached($theme, 'test.htm');
$this->assertTrue($obj->isLoadedFromCache());
$this->assertEquals('<p>Updated test content</p>', $obj->getContent());
$this->assertEquals(filemtime($filePath), $obj->mtime);
/*
* Delete the file. The loadCached() should return null
*/
@unlink($filePath);
$this->assertFileNotExists($filePath);
CmsObject::clearInternalCache();
$obj = TestTemporaryCmsObject::loadCached($theme, 'test.htm');
$this->assertNull($obj);
}
public function testFillFillable()
{
$theme = Theme::load('apitest');
$testContents = 'mytestcontent';
$obj = TestCmsObject::inTheme($theme);
$obj->fill([
'fileName' => 'mytestobj',
'content' => $testContents
]);
$this->assertEquals($testContents, $obj->getContent());
$this->assertEquals('mytestobj.htm', $obj->getFileName());
}
public function testFillNotFillable()
{
$theme = Theme::load('apitest');
$testContents = 'mytestcontent';
$obj = TestCmsObject::inTheme($theme);
$obj->fill([
'something' => 'mytestobj',
'content' => $testContents
]);
$this->assertNull($obj->something);
}
public function testFillInvalidFileNameSymbol()
{
$this->expectException(\Winter\Storm\Exception\ValidationException::class);
$this->expectExceptionMessage('Invalid file name');
$theme = Theme::load('apitest');
$testContents = 'mytestcontent';
$obj = TestCmsObject::inTheme($theme);
$obj->fill([
'fileName' => '@name'
]);
$obj->save();
}
public function testFillInvalidFileNamePath()
{
$this->expectException(\Winter\Storm\Exception\ValidationException::class);
$this->expectExceptionMessage('Invalid file name');
$theme = Theme::load('apitest');
$testContents = 'mytestcontent';
$obj = TestCmsObject::inTheme($theme);
$obj->fill([
'fileName' => '../somefile'
]);
$obj->save();
}
public function testFillInvalidFileSlash()
{
$this->expectException(\Winter\Storm\Exception\ValidationException::class);
$this->expectExceptionMessage('Invalid file name');
$theme = Theme::load('apitest');
$testContents = 'mytestcontent';
$obj = TestCmsObject::inTheme($theme);
$obj->fill([
'fileName' => '/somefile'
]);
$obj->save();
}
public function testFillEmptyFileName()
{
$this->expectException(\Winter\Storm\Exception\ValidationException::class);
$this->expectExceptionMessage('The File Name field is required');
$theme = Theme::load('apitest');
$testContents = 'mytestcontent';
$obj = TestCmsObject::inTheme($theme);
$obj->fill([
'fileName' => ' '
]);
$obj->save();
}
public function testSave()
{
$theme = Theme::load('apitest');
$destFilePath = $theme->getPath() . '/testobjects/mytestobj.htm';
if (file_exists($destFilePath)) {
unlink($destFilePath);
}
$this->assertFileNotExists($destFilePath);
$testContents = 'mytestcontent';
$obj = TestCmsObject::inTheme($theme);
$obj->fill([
'fileName' => 'mytestobj',
'content' => $testContents
]);
$obj->save();
$this->assertFileExists($destFilePath);
$this->assertEquals($testContents, file_get_contents($destFilePath));
}
/**
* @depends testSave
*/
public function testRename()
{
$theme = Theme::load('apitest');
$srcFilePath = $theme->getPath() . '/testobjects/mytestobj.htm';
$this->assertFileExists($srcFilePath);
$destFilePath = $theme->getPath() . '/testobjects/anotherobj.htm';
if (file_exists($destFilePath)) {
unlink($destFilePath);
}
$testContents = 'mytestcontent';
$obj = TestCmsObject::load($theme, 'mytestobj.htm');
$this->assertEquals($testContents, $obj->getContent());
$obj->fill([
'fileName' => 'anotherobj'
]);
$obj->save();
$this->assertFileNotExists($srcFilePath);
$this->assertFileExists($destFilePath);
$this->assertEquals($testContents, file_get_contents($destFilePath));
}
/**
* @depends testRename
*/
public function testSaveSameName()
{
$theme = Theme::load('apitest');
$filePath = $theme->getPath() . '/testobjects/anotherobj.htm';
$this->assertFileExists($filePath);
$testContents = 'new content';
$obj = TestCmsObject::load($theme, 'anotherobj.htm');
$obj->fill([
'fileName' => 'anotherobj',
'content' => $testContents
]);
$obj->save();
$this->assertFileExists($filePath);
$this->assertEquals($testContents, file_get_contents($filePath));
}
/**
* @depends testRename
*/
public function testRenameToExistingFile()
{
$this->expectException(\Winter\Storm\Exception\ApplicationException::class);
$this->expectExceptionMessageMatches('/already\sexists/');
$theme = Theme::load('apitest');
$srcFilePath = $theme->getPath() . '/testobjects/anotherobj.htm';
$this->assertFileExists($srcFilePath);
$destFilePath = $theme->getPath() . '/testobjects/existingobj.htm';
if (!file_exists($destFilePath)) {
file_put_contents($destFilePath, 'str');
}
$this->assertFileExists($destFilePath);
$obj = TestCmsObject::load($theme, 'anotherobj.htm');
$obj->fill(['fileName' => 'existingobj']);
$obj->save();
}
public function testSaveNewDir()
{
$theme = Theme::load('apitest');
$destFilePath = $theme->getPath() . '/testobjects/testsubdir/mytestobj.htm';
if (file_exists($destFilePath)) {
unlink($destFilePath);
}
$destDirPath = dirname($destFilePath);
if (file_exists($destDirPath) && is_dir($destDirPath)) {
rmdir($destDirPath);
}
$this->assertFileNotExists($destFilePath);
$this->assertFileNotExists($destDirPath);
$testContents = 'mytestcontent';
$obj = TestCmsObject::inTheme($theme);
$obj->fill([
'fileName' => 'testsubdir/mytestobj.htm',
'content' => $testContents
]);
$obj->save();
$this->assertFileExists($destFilePath);
$this->assertEquals($testContents, file_get_contents($destFilePath));
}
}

View File

@@ -0,0 +1,322 @@
<?php
namespace Cms\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\CodeParser;
use Cms\Classes\Controller;
use Cms\Classes\Layout;
use Cms\Classes\LayoutCode;
use Cms\Classes\Page;
use Cms\Classes\PageCode;
use Cms\Classes\Theme;
use File;
use ReflectionClass;
class CodeParserTest extends TestCase
{
public function setUp(): void
{
parent::setup();
/*
* Clear cache
*/
foreach (File::directories(storage_path() . '/cms/cache') as $directory) {
File::deleteDirectory($directory);
}
}
public static function getProperty($name)
{
$class = new ReflectionClass(CodeParser::class);
$property = $class->getProperty($name);
$property->setAccessible(true);
return $property;
}
public function testParser()
{
$theme = Theme::load('test');
$layout = Layout::load($theme, 'php-parser-test.htm');
$this->assertNotEmpty($layout);
$parser = new CodeParser($layout);
$info = $parser->parse();
$this->assertIsArray($info);
$this->assertArrayHasKey('filePath', $info);
$this->assertArrayHasKey('className', $info);
$this->assertArrayHasKey('source', $info);
$this->assertFileExists($info['filePath']);
$controller = new Controller($theme);
$obj = $parser->source(null, $layout, $controller);
$this->assertInstanceOf(LayoutCode::class, $obj);
/*
* Test the file contents
*/
$body = preg_replace('/^\s*function/m', 'public function', $layout->code);
$expectedContent = '<?php ' . PHP_EOL;
$expectedContent .= 'class ' . $info['className'] . ' extends ' . LayoutCode::class . PHP_EOL;
$expectedContent .= '{' . PHP_EOL;
$expectedContent .= $body . PHP_EOL;
$expectedContent .= '}' . PHP_EOL;
$this->assertEquals($expectedContent, file_get_contents($info['filePath']));
/*
* Test caching - the first time the file should be parsed
*/
$this->assertEquals('parser', $info['source']);
/*
* Test caching - the second time the file should be loaded from the request-wide cache
*/
$parser = new CodeParser($layout);
$info = $parser->parse();
$this->assertIsArray($info);
$this->assertEquals('request-cache', $info['source']);
$this->assertFileExists($info['filePath']);
/*
* Test caching - reset the request-wide cache and let the parser to load the file from the cache
*/
$property = $this->getProperty('cache');
$property->setValue($parser, []);
$parser = new CodeParser($layout);
$info = $parser->parse();
$this->assertIsArray($info);
$this->assertEquals('cache', $info['source']);
$this->assertFileExists($info['filePath']);
/*
* Test caching - the cached data should now be stored in the request-wide cache again
*/
$parser = new CodeParser($layout);
$info = $parser->parse();
$this->assertIsArray($info);
$this->assertEquals('request-cache', $info['source']);
$this->assertFileExists($info['filePath']);
/*
* Test caching - update the file modification time and reset the internal cache. The file should be parsed.
*/
$this->assertTrue(@touch($layout->getFilePath()));
clearstatcache();
$layout = Layout::load($theme, 'php-parser-test.htm');
$this->assertNotEmpty($layout);
$parser = new CodeParser($layout);
$property->setValue($parser, []);
$info = $parser->parse();
$this->assertIsArray($info);
$this->assertEquals('parser', $info['source']);
$this->assertFileExists($info['filePath']);
}
public function testParseNoPhp()
{
$theme = Theme::load('test');
$layout = Layout::load($theme, 'no-php.htm');
$this->assertNotEmpty($layout);
$parser = new CodeParser($layout);
$info = $parser->parse();
$this->assertIsArray($info);
$this->assertArrayHasKey('filePath', $info);
$this->assertArrayHasKey('className', $info);
$this->assertArrayHasKey('source', $info);
$this->assertFileExists($info['filePath']);
$expectedContent = '<?php ' . PHP_EOL;
$expectedContent .= 'class ' . $info['className'] . ' extends ' . LayoutCode::class . PHP_EOL;
$expectedContent .= '{' . PHP_EOL;
$expectedContent .= PHP_EOL;
$expectedContent .= '}' . PHP_EOL;
$this->assertEquals($expectedContent, file_get_contents($info['filePath']));
}
public function testParsePage()
{
$theme = Theme::load('test');
$page = Page::load($theme, 'cycle-test.htm');
$this->assertNotEmpty($page);
$parser = new CodeParser($page);
$info = $parser->parse();
$this->assertIsArray($info);
$this->assertArrayHasKey('filePath', $info);
$this->assertArrayHasKey('className', $info);
$this->assertArrayHasKey('source', $info);
$this->assertFileExists($info['filePath']);
$controller = new Controller($theme);
$obj = $parser->source($page, null, $controller);
$this->assertInstanceOf(PageCode::class, $obj);
$body = preg_replace('/^\s*function/m', 'public function', $page->code);
$expectedContent = '<?php ' . PHP_EOL;
$expectedContent .= 'class ' . $info['className'] . ' extends ' . PageCode::class . PHP_EOL;
$expectedContent .= '{' . PHP_EOL;
$expectedContent .= $body . PHP_EOL;
$expectedContent .= '}' . PHP_EOL;
$this->assertEquals($expectedContent, file_get_contents($info['filePath']));
}
public function testOptionalPhpTags()
{
$theme = Theme::load('test');
/*
* Test short PHP tags
*/
$page = Page::load($theme, 'optional-short-php-tags.htm');
$this->assertNotEmpty($page);
$parser = new CodeParser($page);
$info = $parser->parse();
$this->assertIsArray($info);
$this->assertArrayHasKey('filePath', $info);
$this->assertArrayHasKey('className', $info);
$this->assertArrayHasKey('source', $info);
$this->assertFileExists($info['filePath']);
$controller = new Controller($theme);
$obj = $parser->source($page, null, $controller);
$this->assertInstanceOf('\Cms\Classes\PageCode', $obj);
$body = preg_replace('/^\s*function/m', 'public function', $page->code);
$expectedContent = '<?php ' . PHP_EOL;
$expectedContent .= 'class ' . $info['className'] . ' extends ' . PageCode::class . PHP_EOL;
$expectedContent .= '{' . PHP_EOL;
$expectedContent .= $body . PHP_EOL;
$expectedContent .= '}' . PHP_EOL;
$this->assertEquals($expectedContent, file_get_contents($info['filePath']));
/*
* Test full PHP tags
*/
$page = Page::load($theme, 'optional-full-php-tags.htm');
$this->assertNotEmpty($page);
$parser = new CodeParser($page);
$info = $parser->parse();
$this->assertIsArray($info);
$this->assertArrayHasKey('filePath', $info);
$this->assertArrayHasKey('className', $info);
$this->assertArrayHasKey('source', $info);
$this->assertFileExists($info['filePath']);
$controller = new Controller($theme);
$obj = $parser->source($page, null, $controller);
$this->assertInstanceOf(PageCode::class, $obj);
$body = preg_replace('/^\s*function/m', 'public function', $page->code);
$expectedContent = '<?php ' . PHP_EOL;
$expectedContent .= 'class ' . $info['className'] . ' extends ' . PageCode::class . PHP_EOL;
$expectedContent .= '{' . PHP_EOL;
$expectedContent .= $body . PHP_EOL;
$expectedContent .= '}' . PHP_EOL;
$this->assertEquals($expectedContent, file_get_contents($info['filePath']));
}
// public function testSyntaxErrors()
// {
// $this->markTestIncomplete('Test PHP parsing errors here.');
// }
public function testNamespaces()
{
$theme = Theme::load('test');
$page = Page::load($theme, 'code-namespaces.htm');
$this->assertNotEmpty($page);
$parser = new CodeParser($page);
$info = $parser->parse();
$this->assertIsArray($info);
$this->assertArrayHasKey('filePath', $info);
$this->assertArrayHasKey('className', $info);
$this->assertArrayHasKey('source', $info);
$this->assertFileExists($info['filePath']);
$controller = new Controller($theme);
$obj = $parser->source($page, null, $controller);
$this->assertInstanceOf(PageCode::class, $obj);
$referenceFilePath = base_path() . '/modules/cms/tests/fixtures/reference/namespaces.php.stub';
$this->assertFileExists($referenceFilePath);
$referenceContents = $this->getContents($referenceFilePath);
$referenceContents = str_replace('{className}', $info['className'], $referenceContents);
$this->assertEquals($referenceContents, $this->getContents($info['filePath']));
}
public function testNamespacesAliases()
{
$theme = Theme::load('test');
$page = Page::load($theme, 'code-namespaces-aliases.htm');
$this->assertNotEmpty($page);
$parser = new CodeParser($page);
$info = $parser->parse();
$this->assertIsArray($info);
$this->assertArrayHasKey('filePath', $info);
$this->assertArrayHasKey('className', $info);
$this->assertArrayHasKey('source', $info);
$this->assertFileExists($info['filePath']);
$controller = new Controller($theme);
$obj = $parser->source($page, null, $controller);
$this->assertInstanceOf(PageCode::class, $obj);
$referenceFilePath = base_path() . '/modules/cms/tests/fixtures/reference/namespaces-aliases.php.stub';
$this->assertFileExists($referenceFilePath);
$referenceContents = $this->getContents($referenceFilePath);
$referenceContents = str_replace('{className}', $info['className'], $referenceContents);
$this->assertEquals($referenceContents, $this->getContents($info['filePath']));
}
//
// Helpers
//
protected function getContents($path)
{
$content = file_get_contents($path);
$content = preg_replace('~\R~u', PHP_EOL, $content); // Normalize EOL
return $content;
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace Cms\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\CodeParser;
use Cms\Classes\ComponentManager;
use Cms\Classes\Controller;
use Cms\Classes\Layout;
use Cms\Classes\Page;
use Cms\Classes\Theme;
class ComponentManagerTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/Archive.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/Post.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/MainMenu.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/ContentBlock.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/Comments.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/classes/Users.php';
}
public function testListComponents()
{
$manager = ComponentManager::instance();
$components = $manager->listComponents();
$this->assertArrayHasKey('testArchive', $components);
$this->assertArrayHasKey('testPost', $components);
}
public function testListComponentDetails()
{
$manager = ComponentManager::instance();
$components = $manager->listComponentDetails();
$this->assertArrayHasKey('testArchive', $components);
$this->assertArrayHasKey('name', $components['testArchive']);
$this->assertArrayHasKey('description', $components['testArchive']);
$this->assertEquals('Blog Archive Dummy Component', $components['testArchive']['name']);
$this->assertEquals('Displays an archive of blog posts.', $components['testArchive']['description']);
$this->assertArrayHasKey('testPost', $components);
$this->assertArrayHasKey('name', $components['testPost']);
$this->assertArrayHasKey('description', $components['testPost']);
$this->assertEquals('Blog Post Dummy Component', $components['testPost']['name']);
$this->assertEquals('Displays a blog post.', $components['testPost']['description']);
}
public function testGetComponentWithFactoryUsingAutomaticResolution()
{
$manager = ComponentManager::instance();
$components = $manager->listComponentDetails();
$this->assertArrayHasKey('testComments', $components);
$this->assertArrayHasKey('name', $components['testComments']);
$this->assertArrayHasKey('description', $components['testComments']);
$this->assertEquals('Blog Comments Dummy Component', $components['testComments']['name']);
$this->assertEquals('Displays the list of comments on a post.', $components['testComments']['description']);
$comments = $manager->makeComponent('testComments', $this->spoofPageCode(), []);
$users = $comments->getUsers()->getUsers();
$this->assertArrayHasKey('Art Vandelay', $users);
$this->assertArrayHasKey('Carl', $users);
$this->assertEquals('Arquitecht and Importer/Exporter', $users['Art Vandelay']);
$this->assertEquals('where is he?', $users['Carl']);
}
public function testFindByAlias()
{
$manager = ComponentManager::instance();
$component = $manager->resolve('testArchive');
$this->assertEquals('\Winter\Tester\Components\Archive', $component);
$component = $manager->resolve('testPost');
$this->assertEquals('\Winter\Tester\Components\Post', $component);
}
public function testHasComponent()
{
$manager = ComponentManager::instance();
$result = $manager->hasComponent('testArchive');
$this->assertTrue($result);
$result = $manager->hasComponent('Winter\Tester\Components\Archive');
$this->assertTrue($result);
$result = $manager->hasComponent('Winter\Tester\Components\Post');
$this->assertTrue($result);
}
public function testMakeComponent()
{
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/Archive.php';
$pageObj = $this->spoofPageCode();
// Test a defined property
$manager = ComponentManager::instance();
$object = $manager->makeComponent('testArchive', $pageObj, ['posts-per-page' => 20]);
$this->assertNotNull($object);
$this->assertEquals(20, $object->property('posts-per-page'));
// Test an undefined property with default
$object = $manager->makeComponent('testArchive', $pageObj);
$this->assertNotNull($object);
$this->assertEquals(10, $object->property('posts-per-page'));
$this->assertEquals(2020, $object->property('undefined-property', 2020));
}
public function testDefineProperties()
{
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/Archive.php';
$manager = ComponentManager::instance();
$object = $manager->makeComponent('testArchive');
$details = $object->componentDetails();
$this->assertCount(2, $details);
$this->assertNotNull($details);
$this->assertArrayHasKey('name', $details);
$this->assertArrayHasKey('description', $details);
$this->assertEquals('Blog Archive Dummy Component', $details['name']);
}
private function spoofPageCode()
{
// Spoof all the objects we need to make a page object
$theme = Theme::load('test');
$page = Page::load($theme, 'index.htm');
$layout = Layout::load($theme, 'content.htm');
$controller = new Controller($theme);
$parser = new CodeParser($page);
$pageObj = $parser->source($page, $layout, $controller);
return $pageObj;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Cms\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\Content;
use Cms\Classes\Theme;
class ContentTest extends TestCase
{
public function testMarkdownContent()
{
$theme = Theme::load('test');
$content = Content::load($theme, 'markdown-content.md');
$this->assertEquals('Be brave, be **bold**, live *italic*', $content->markup);
$this->assertEquals("<p>Be brave, be <strong>bold</strong>, live <em>italic</em></p>\n", $content->parsedMarkup);
}
public function testTextContent()
{
$theme = Theme::load('test');
$content = Content::load($theme, 'text-content.txt');
$this->assertEquals('Pen is <mightier> than the sword, HTML is <richer> than the text', $content->markup);
$this->assertEquals('Pen is &lt;mightier&gt; than the sword, HTML is &lt;richer&gt; than the text', $content->parsedMarkup);
}
public function testHtmlContent()
{
$theme = Theme::load('test');
$content = Content::load($theme, 'html-content.htm');
$this->assertEquals('<a href="#">Stephen Saucier</a> changed his profile picture &mdash; <small>7 hrs ago</small></div>', $content->markup);
$this->assertEquals('<a href="#">Stephen Saucier</a> changed his profile picture &mdash; <small>7 hrs ago</small></div>', $content->parsedMarkup);
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace Cms\Tests\Classes;
use Cms\Classes\Controller;
use Cms\Classes\Theme;
use Illuminate\Support\Facades\Request;
use System\Tests\Bootstrap\TestCase;
use Winter\Storm\Exception\SystemException;
use Winter\Storm\Halcyon\Model;
use Winter\Storm\Support\Facades\Config;
class ControllerPostbackTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
Config::set('cms.themesPath', '/modules/cms/tests/fixtures/themes');
Model::clearBootedModels();
Model::flushEventListeners();
}
/**
* Builds a mock Request that simulates an AJAX POST with the given handler.
*/
protected function configAjaxRequestMock(string $handler, $partials = false)
{
$requestMock = $this
->getMockBuilder('Illuminate\Http\Request')
->disableOriginalConstructor()
->setMethods(['ajax', 'method', 'header'])
->getMock();
$map = [
['X_WINTER_REQUEST_HANDLER', null, $handler],
['X_WINTER_REQUEST_PARTIALS', null, $partials],
];
$requestMock->expects($this->any())
->method('ajax')
->will($this->returnValue(true));
$requestMock->expects($this->any())
->method('method')
->will($this->returnValue('POST'));
$requestMock->expects($this->any())
->method('header')
->will($this->returnValueMap($map));
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'])
->getMock();
$requestMock->expects($this->any())
->method('ajax')
->will($this->returnValue(false));
$requestMock->expects($this->any())
->method('method')
->will($this->returnValue('POST'));
$requestMock->expects($this->any())
->method('header')
->will($this->returnValue(null));
$postData = ['_handler' => $handler];
$requestMock->expects($this->any())
->method('post')
->will($this->returnCallback(function ($key = null, $default = null) use ($postData) {
if ($key === null) {
return $postData;
}
return $postData[$key] ?? $default;
}));
$requestMock->expects($this->any())
->method('input')
->will($this->returnCallback(function ($key = null, $default = null) use ($postData) {
if ($key === null) {
return $postData;
}
return $postData[$key] ?? $default;
}));
return $requestMock;
}
//
// AJAX header path — validates handler name (existing behavior)
//
public function testAjaxPathRejectsInvalidHandlerName(): void
{
$this->expectException(SystemException::class);
$this->expectExceptionMessage('Invalid AJAX handler name: update_onDelete.');
Request::swap($this->configAjaxRequestMock('update_onDelete'));
$theme = Theme::load('test');
$controller = new Controller($theme);
$controller->run('/ajax-test');
}
public function testAjaxPathAcceptsValidHandlerName(): void
{
// onTest exists on the ajax-test page, so this should not throw SystemException for invalid name
// It may throw for other reasons (missing partials, etc.) but not for handler name validation
Request::swap($this->configAjaxRequestMock('onTest', ''));
$theme = Theme::load('test');
$controller = new Controller($theme);
try {
$controller->run('/ajax-test');
} catch (SystemException $e) {
$this->assertStringNotContainsString('Invalid AJAX handler name', $e->getMessage());
}
$this->assertTrue(true);
}
//
// Postback _handler path — validates handler name (our fix)
//
public function testPostbackPathRejectsInvalidHandlerName(): void
{
$this->expectException(SystemException::class);
$this->expectExceptionMessage('Invalid AJAX handler name: update_onDelete.');
Config::set('cms.enableCsrfProtection', false);
Request::swap($this->configPostbackRequestMock('update_onDelete'));
$theme = Theme::load('test');
$controller = new Controller($theme);
$controller->run('/ajax-test');
}
public function testPostbackPathRejectsMethodName(): void
{
$this->expectException(SystemException::class);
$this->expectExceptionMessage('Invalid AJAX handler name: execPageCycle.');
Config::set('cms.enableCsrfProtection', false);
Request::swap($this->configPostbackRequestMock('execPageCycle'));
$theme = Theme::load('test');
$controller = new Controller($theme);
$controller->run('/ajax-test');
}
public function testPostbackPathRejectsActionPrefixedHandler(): void
{
$this->expectException(SystemException::class);
$this->expectExceptionMessage('Invalid AJAX handler name: index_onSave.');
Config::set('cms.enableCsrfProtection', false);
Request::swap($this->configPostbackRequestMock('index_onSave'));
$theme = Theme::load('test');
$controller = new Controller($theme);
$controller->run('/ajax-test');
}
}

View File

@@ -0,0 +1,752 @@
<?php
namespace Cms\Tests\Classes;
use Cms;
use Request;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\Theme;
use Cms\Classes\Controller;
use System\Helpers\View;
use Winter\Storm\Halcyon\Model;
use Winter\Storm\Support\Facades\Config;
class ControllerTest extends TestCase
{
protected string $origThemePath;
public function setUp(): void
{
parent::setUp();
$this->origThemePath = Config::get('cms.themesPath');
// Set temporary themes path for tests
Config::set('cms.themesPath', '/modules/cms/tests/fixtures/themes');
Model::clearBootedModels();
Model::flushEventListeners();
View::clearVarCache();
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/Archive.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/Post.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/MainMenu.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/ContentBlock.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/Comments.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/classes/Users.php';
}
public function tearDown(): void
{
// Restore original themes path
Config::set('cms.themesPath', $this->origThemePath);
parent::tearDown();
}
public function testThemeUrl()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$url = $controller->themeUrl();
$this->assertEquals(url('/modules/cms/tests/fixtures/themes/test'), $url);
$url = $controller->themeUrl('assets/css/style1.css');
$this->assertEquals(url('/modules/cms/tests/fixtures/themes/test/assets/css/style1.css'), $url);
$pathSymbolTests = [
'~/modules/cms/tests/fixtures/themes/test/assets/css/style1.css' => '/',
'$/fakeauthor/fakeplugin/assets/src/app.js' => '/plugins/',
'#/faketheme/assets/css/style1.css' => '/themes/',
];
foreach ($pathSymbolTests as $symbolizedPath => $urlPrefix) {
$url = $controller->themeUrl($symbolizedPath);
$this->assertEquals(url(str_replace(substr($symbolizedPath, 0, 2), $urlPrefix, $symbolizedPath)), $url);
}
}
public function testThemeCombineAssets(): void
{
$theme = Theme::load('test');
$controller = new Controller($theme);
// Generate a url
$url = $controller->themeUrl(['~/modules/cms/tests/fixtures/themes/test/assets/css/style1.css', 'assets/css/style2.css']);
$this->assertIsString($url);
// Grab the cache key from the url
$cacheKey = 'combiner.' . str_before(basename($url), '-');
// Load the cached config
$combinerConfig = \Cache::get($cacheKey);
$this->assertIsString($combinerConfig);
// Decode the config
$combinerConfig = unserialize(base64_decode($combinerConfig));
// Assert the result is an array and includes files
$this->assertIsArray($combinerConfig);
$this->assertArrayHasKey('files', $combinerConfig);
$this->assertCount(2, $combinerConfig['files']);
// Check our input file names against our output file names
$files = array_map('basename', $combinerConfig['files']);
$this->assertTrue(in_array('style1.css', $files));
$this->assertTrue(in_array('style2.css', $files));
}
public function testPageUrl()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$loadUrl = '/filters-test/current-slug';
$response = $controller->run($loadUrl);
// Check pageUrl for current page
$url = $controller->pageUrl('');
$this->assertEquals(url($loadUrl), $url);
// Check pageUrl for persistent URL parameters
$url = $controller->pageUrl('blog-post');
$this->assertEquals(url('/blog/post/current-slug'), $url);
// Check pageUrl for providing values for URL parameters
$url = $controller->pageUrl('blog-post', ['url_title' => 'test-slug']);
$this->assertEquals(url('/blog/post/test-slug'), $url);
// Check pageUrl for disabling persistent URL parameters
$url = $controller->pageUrl('blog-post', [], false);
$this->assertEquals(url('/blog/post/default'), $url);
// Check pageUrl for disabling persistent URL parameters with the second argument being routePersistence
$url = $controller->pageUrl('blog-post', false);
$this->assertEquals(url('/blog/post/default'), $url);
// Check the Twig render results
$results = $response->getContent();
$lines = explode("\n", str_replace("\r\n", "\n", $results));
foreach ($lines as $test) {
list($result, $expected) = explode(' -> ', $test);
$this->assertEquals($expected, $result);
}
}
public function test404()
{
/*
* Test the built-in 404 page
*/
$theme = Theme::load('apitest');
$controller = new Controller($theme);
$response = $controller->run('/some-page-that-doesnt-exist');
$this->assertNotEmpty($response);
$this->assertInstanceOf('\Illuminate\Http\Response', $response);
ob_start();
include base_path() . '/modules/cms/views/404.php';
$page404Content = ob_get_contents();
ob_end_clean();
$this->assertEquals($page404Content, $response->getContent());
/*
* Test the theme 404 page
*/
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/some-page-that-doesnt-exist');
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
$content = $response->getContent();
$this->assertIsString($content);
$this->assertEquals('<p>Page not found</p>', $content);
}
public function testRoot()
{
/*
* Test the / route and the fallback layout
*/
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/');
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
$content = $response->getContent();
$this->assertIsString($content);
$this->assertEquals('<h1>My Webpage</h1>', trim($content));
}
public function testLayoutNotFound()
{
$this->expectException(\Winter\Storm\Exception\SystemException::class);
$this->expectExceptionMessageMatches('/is\snot\sfound/');
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/no-layout');
}
public function testExistingLayout()
{
/*
* Test existing layout
*/
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/with-layout');
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
$content = $response->getContent();
$this->assertEquals('<div><p>Hey</p></div>', $content);
}
public function testPartials()
{
/*
* Test partials referred in the layout and page
*/
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/with-partials')->getContent();
$this->assertEquals('<div>LAYOUT PARTIAL<p>Hey PAGE PARTIAL Homer Simpson A partial</p></div>', $response);
}
public function testChildThemePartials()
{
/*
* Test partials referred in the layout and page
*/
$theme = Theme::load('childtest');
$controller = new Controller($theme);
$response = $controller->run('/with-partials')->getContent();
$this->assertEquals('<div>LAYOUT PARTIAL<p>Hey PAGE PARTIAL Homer Simpson A child partial</p></div>', $response);
}
public function testContent()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/with-content')->getContent();
$this->assertEquals('<div>LAYOUT CONTENT<p>Hey PAGE CONTENT A content</p></div>', $response);
}
public function testChildThemeContent()
{
$theme = Theme::load('childtest');
$controller = new Controller($theme);
$response = $controller->run('/with-content')->getContent();
$this->assertEquals('<div>LAYOUT CONTENT<p>Hey PAGE CONTENT A child content</p></div>', $response);
}
public function testBlocks()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/with-placeholder')->getContent();
$this->assertEquals("<div>LAYOUT CONTENT <span>BLOCK\n DEFAULT</span> <p>Hey PAGE CONTENT</p></div>SECOND BLOCK", $response);
}
public function testChildThemeBlocks()
{
$theme = Theme::load('childtest');
$controller = new Controller($theme);
$response = $controller->run('/with-placeholder')->getContent();
$this->assertEquals("<div>LAYOUT CONTENT <span>BLOCK\n DEFAULT</span> <p>Hey PAGE CONTENT</p></div>SECOND BLOCK", $response);
}
public function testLayoutInSubdirectory()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/apage')->getContent();
$this->assertEquals("<div>LAYOUT CONTENT <h1>This page is a subdirectory</h1></div>", $response);
}
public function testPartialNotFound()
{
$this->expectException(\Twig\Error\RuntimeError::class);
$this->expectExceptionMessageMatches('/is\snot\sfound/');
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/no-partial')->getContent();
}
public function testChildThemePartialNotFound()
{
$this->expectException(\Twig\Error\RuntimeError::class);
$this->expectExceptionMessageMatches('/is\snot\sfound/');
$theme = Theme::load('childtest');
$controller = new Controller($theme);
$response = $controller->run('/no-partial')->getContent();
}
public function testPageLifeCycle()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/cycle-test')->getContent();
$this->assertEquals('12345', $response);
}
public function testChildThemePageLifeCycle()
{
$theme = Theme::load('childtest');
$controller = new Controller($theme);
$response = $controller->run('/cycle-test')->getContent();
$this->assertEquals('12345', $response);
}
protected function configAjaxRequestMock($handler, $partials = false)
{
$requestMock = $this
->getMockBuilder('Illuminate\Http\Request')
->disableOriginalConstructor()
->setMethods(['ajax', 'method', 'header'])
->getMock();
$map = [
['X_WINTER_REQUEST_HANDLER', null, $handler],
['X_WINTER_REQUEST_PARTIALS', null, $partials],
];
$requestMock->expects($this->any())
->method('ajax')
->will($this->returnValue(true));
$requestMock->expects($this->any())
->method('method')
->will($this->returnValue('POST'));
$requestMock->expects($this->any())
->method('header')
->will($this->returnValueMap($map));
return $requestMock;
}
public function testAjaxHandlerNotFound()
{
$this->expectException(\Winter\Storm\Exception\SystemException::class);
$this->expectExceptionMessage('AJAX handler \'onNoHandler\' was not found.');
Request::swap($this->configAjaxRequestMock('onNoHandler', ''));
$theme = Theme::load('test');
$controller = new Controller($theme);
$controller->run('/ajax-test');
}
public function testAjaxInvalidHandlerName()
{
$this->expectException(\Winter\Storm\Exception\SystemException::class);
$this->expectExceptionMessage('Invalid AJAX handler name: delete.');
Request::swap($this->configAjaxRequestMock('delete'));
$theme = Theme::load('test');
$controller = new Controller($theme);
$controller->run('/ajax-test');
}
public function testAjaxInvalidPartial()
{
$this->expectException(\Winter\Storm\Exception\SystemException::class);
$this->expectExceptionMessage('Invalid partial name: p:artial.');
Request::swap($this->configAjaxRequestMock('onTest', 'p:artial'));
$theme = Theme::load('test');
$controller = new Controller($theme);
$controller->run('/ajax-test');
}
public function testAjaxPartialNotFound()
{
$this->expectException(\Winter\Storm\Exception\SystemException::class);
$this->expectExceptionMessage('The partial \'partial\' is not found.');
Request::swap($this->configAjaxRequestMock('onTest', 'partial'));
$theme = Theme::load('test');
$controller = new Controller($theme);
$controller->run('/ajax-test');
}
public function testPageAjax()
{
Request::swap($this->configAjaxRequestMock('onTest', 'ajax-result'));
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/ajax-test');
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
$content = $response->getOriginalContent();
$this->assertIsArray($content);
$this->assertEquals(200, $response->getStatusCode());
$this->assertCount(1, $content);
$this->assertArrayHasKey('ajax-result', $content);
$this->assertEquals('page', $content['ajax-result']);
}
public function testLayoutAjax()
{
Request::swap($this->configAjaxRequestMock('onTestLayout', 'ajax-result'));
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/ajax-test');
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
$content = $response->getOriginalContent();
$this->assertIsArray($content);
$this->assertEquals(200, $response->getStatusCode());
$this->assertCount(1, $content);
$this->assertArrayHasKey('ajax-result', $content);
$this->assertEquals('layout-test', $content['ajax-result']);
}
public function testAjaxMultiplePartials()
{
Request::swap($this->configAjaxRequestMock('onTest', 'ajax-result&ajax-second-result'));
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/ajax-test');
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
$content = $response->getOriginalContent();
$this->assertIsArray($content);
$this->assertEquals(200, $response->getStatusCode());
$this->assertCount(2, $content);
$this->assertArrayHasKey('ajax-result', $content);
$this->assertArrayHasKey('ajax-second-result', $content);
$this->assertEquals('page', $content['ajax-result']);
$this->assertEquals('second', $content['ajax-second-result']);
}
public function testBasicComponents()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/with-component')->getContent();
$page = self::getProtectedProperty($controller, 'page');
$this->assertArrayHasKey('testArchive', $page->components);
$component = $page->components['testArchive'];
$details = $component->componentDetails();
$content = <<<ESC
<div>LAYOUT CONTENT<p>This page uses components.</p>
<h3>Lorum ipsum</h3>
<p>Post Content #1</p>
<h3>La Playa Nudista</h3>
<p>Second Post Content</p>
</div>
ESC;
$this->assertEquals(str_replace(PHP_EOL, "\n", $content), $response);
$this->assertEquals(69, $component->property('posts-per-page'));
$this->assertEquals('Blog Archive Dummy Component', $details['name']);
$this->assertEquals('Displays an archive of blog posts.', $details['description']);
}
public function testComponentAliases()
{
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/Archive.php';
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/with-components')->getContent();
$page = self::getProtectedProperty($controller, 'page');
$this->assertArrayHasKey('firstAlias', $page->components);
$this->assertArrayHasKey('secondAlias', $page->components);
$component = $page->components['firstAlias'];
$component2 = $page->components['secondAlias'];
$content = <<<ESC
<div>LAYOUT CONTENT<p>This page uses components.</p>
<h3>Lorum ipsum</h3>
<p>Post Content #1</p>
<h3>La Playa Nudista</h3>
<p>Second Post Content</p>
</div>
ESC;
$this->assertEquals(str_replace(PHP_EOL, "\n", $content), $response);
$this->assertEquals(6, $component->property('posts-per-page'));
$this->assertEquals(9, $component2->property('posts-per-page'));
}
public function testComponentAjax()
{
Request::swap($this->configAjaxRequestMock('testArchive::onTestAjax', 'ajax-result'));
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/with-component');
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
$content = $response->getOriginalContent();
$this->assertIsArray($content);
$this->assertEquals(200, $response->getStatusCode());
$this->assertCount(1, $content);
$this->assertArrayHasKey('ajax-result', $content);
$this->assertEquals('page', $content['ajax-result']);
}
public function testComponentClassNotFound()
{
$this->expectException(\Winter\Storm\Exception\SystemException::class);
$this->expectExceptionMessageMatches('/is\snot\sregistered\sfor\sthe\scomponent/');
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/no-component-class')->getContent();
}
public function testSoftComponentClassNotFound()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/no-soft-component-class')->getContent();
$this->assertEquals('<p>Hey</p>', $response);
}
public function testSoftComponentClassFound()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/with-soft-component-class')->getContent();
$page = $controller->getPage();
$this->assertArrayHasKey('testArchive', $page->components);
$component = $page->components['testArchive'];
$details = $component->componentDetails();
$content = <<<ESC
<div>LAYOUT CONTENT<p>This page uses components.</p>
<h3>Lorum ipsum</h3>
<p>Post Content #1</p>
<h3>La Playa Nudista</h3>
<p>Second Post Content</p>
</div>
ESC;
$this->assertEquals(str_replace(PHP_EOL, "\n", $content), $response);
$this->assertEquals(69, $component->property('posts-per-page'));
$this->assertEquals('Blog Archive Dummy Component', $details['name']);
$this->assertEquals('Displays an archive of blog posts.', $details['description']);
}
public function testSoftComponentWithAliasClassFound()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/with-soft-component-class-alias')->getContent();
$page = $controller->getPage();
$this->assertArrayHasKey('someAlias', $page->components);
$component = $page->components['someAlias'];
$details = $component->componentDetails();
$content = <<<ESC
<div>LAYOUT CONTENT<p>This page uses components.</p>
<h3>Lorum ipsum</h3>
<p>Post Content #1</p>
<h3>La Playa Nudista</h3>
<p>Second Post Content</p>
</div>
ESC;
$this->assertEquals(str_replace(PHP_EOL, "\n", $content), $response);
$this->assertEquals(69, $component->property('posts-per-page'));
$this->assertEquals('Blog Archive Dummy Component', $details['name']);
$this->assertEquals('Displays an archive of blog posts.', $details['description']);
}
public function testComponentNotFound()
{
//
// This test should probably be throwing an exception... -sg
//
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/no-component')->getContent();
$this->assertEquals('<p>Hey</p>', $response);
}
public function testComponentPartial()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/component-partial')->getContent();
$this->assertEquals('<p>DEFAULT MARKUP: I am a post yay</p>', $response);
}
public function testComponentPartialAliasOverride()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/component-partial-alias-override')->getContent();
//
// Testing case sensitivity
//
// Component alias: overRide1
// Target path: partials\override1\default.htm
//
$this->assertEquals('<p>I am an override alias partial! Yay</p>', $response);
}
public function testComponentPartialOverride()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/component-partial-override')->getContent();
//
// Testing case sensitivity
//
// Component code: testPost
// Target path: partials\testpost\default.htm
//
$this->assertEquals('<p>I am an override partial! Yay</p>', $response);
}
public function testComponentPartialNesting()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/component-partial-nesting')->getContent();
$content = <<<ESC
<h1>Level 1</h1>
<ul>
<strong>Home</strong>
<strong>Blog</strong>
<strong>About</strong>
<strong>Contact</strong>
<strong>Home</strong>
<strong>Blog</strong>
<strong>About</strong>
<strong>Contact</strong>
<strong>Home</strong>
<strong>Blog</strong>
<strong>About</strong>
<strong>Contact</strong>
</ul>
<h1>Level 2</h1>
<p>DEFAULT MARKUP: I am a post yay</p><p>I am another post, deep down</p>
<h1>Level 3</h1>
<h4>DEFAULT MARKUP: Menu</h4>
<ul>
<li>DEFAULT: Home</li>
<li>DEFAULT: Blog</li>
<li>DEFAULT: About</li>
<li>DEFAULT: Contact</li>
</ul>
<p>Insert post here</p>
ESC;
$this->assertEquals(str_replace(PHP_EOL, "\n", $content), $response);
}
public function testComponentWithOnRender()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/component-custom-render')->getContent();
$content = <<<ESC
Pass
Custom output: Would you look over Picasso's shoulder
Custom output: And tell him about his brush strokes?
ESC;
$this->assertEquals(str_replace(PHP_EOL, "\n", $content), $response);
}
public function testMacro()
{
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/with-macro')->getContent();
$this->assertStringContainsString(
'<p><a href="' . Cms::url('/') . '">with-macro.htm</a><strong>with-macro.htm</strong></p>',
$response
);
}
public function testSharedVariable()
{
$this->app['view']->share('winterStatus', 'Is Awesome');
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/shared-variable')->getContent();
$this->assertStringContainsString(
'<p>Winter Is Awesome</p>',
$response
);
$this->assertStringContainsString(
'<p>/shared-variable</p>',
$response
);
}
public function testSharedVariableCannotOverrideSystemGlobals()
{
$this->app['view']->share('winterStatus', 'Is Awesome');
// This override should not apply and change the page URL in the fixture template
$this->app['view']->share('this', [
'page' => [
'url' => '/overriden',
],
]);
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/shared-variable')->getContent();
$this->assertStringContainsString(
'<p>Winter Is Awesome</p>',
$response
);
$this->assertStringContainsString(
'<p>/shared-variable</p>',
$response
);
}
public function testSharedVariableCanBeOverriddenLocally()
{
$this->app['view']->share('winterStatus', 'Is Awesome');
$theme = Theme::load('test');
$controller = new Controller($theme);
$response = $controller->run('/shared-variable-override')->getContent();
$this->assertStringContainsString(
'<p>Winter Is Coming</p>',
$response
);
$this->assertStringContainsString(
'<p>/shared-variable-override</p>',
$response
);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Cms\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\Controller;
use Cms\Classes\Page;
use Cms\Classes\Theme;
class PageTest extends TestCase
{
public function testResolveMenuItem()
{
$theme = Theme::load('test');
$controller = new Controller;
$item = (object) [
'type' => 'cms-page',
'reference' => 'index',
];
// Check to make sure that resolved menuItems for the provided URL are considered active
// with or without a trailing slash
$url = $controller->pageUrl($item->reference);
$trailingUrl = $url . '/';
$result = Page::resolveMenuItem($item, $url, $theme);
$this->assertTrue($result['isActive']);
$result = Page::resolveMenuItem($item, $trailingUrl, $theme);
$this->assertTrue($result['isActive']);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Cms\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\PartialStack;
class PartialStackTest extends TestCase
{
public function testStackPartials()
{
$stack = new PartialStack;
/*
* Stack em up
*/
$stack->stackPartial();
$stack->addComponent('override1', 'Winter\Tester\Components\MainMenu');
$stack->addComponent('override2', 'Winter\Tester\Components\ContentBlock');
$stack->stackPartial();
$stack->addComponent('override3', 'Winter\Tester\Components\Post');
$stack->addComponent('post', 'Winter\Tester\Components\Post');
$stack->stackPartial();
$stack->addComponent('mainMenu', 'Winter\Tester\Components\MainMenu');
/*
* Knock em down
*/
$this->assertEquals('Winter\Tester\Components\MainMenu', $stack->getComponent('mainMenu'));
$this->assertEquals('Winter\Tester\Components\MainMenu', $stack->getComponent('override1'));
$stack->unstackPartial();
$this->assertNull($stack->getComponent('mainMenu'));
$this->assertEquals('Winter\Tester\Components\ContentBlock', $stack->getComponent('override2'));
$this->assertEquals('Winter\Tester\Components\Post', $stack->getComponent('override3'));
$stack->unstackPartial();
$this->assertNull($stack->getComponent('mainMenu'));
$this->assertNull($stack->getComponent('post'));
$this->assertEquals('Winter\Tester\Components\MainMenu', $stack->getComponent('override1'));
$stack->unstackPartial();
$this->assertNull($stack->getComponent('post'));
$this->assertNull($stack->getComponent('mainMenu'));
$this->assertNull($stack->getComponent('override1'));
$this->assertNull($stack->getComponent('override2'));
$this->assertNull($stack->getComponent('override3'));
}
public function testEmptyStack()
{
$stack = new PartialStack;
$this->assertNull($stack->getComponent('xxx'));
}
}

View File

@@ -0,0 +1,183 @@
<?php
namespace Cms\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\Router;
use Cms\Classes\Theme;
use ReflectionClass;
class RouterTest extends TestCase
{
protected static $theme = null;
public function setUp(): void
{
parent::setUp();
self::$theme = Theme::load('test');
}
protected static function getMethod($name)
{
$class = new ReflectionClass('\Cms\Classes\Router');
$method = $class->getMethod($name);
$method->setAccessible(true);
return $method;
}
public static function getProperty($name)
{
$class = new ReflectionClass('\Cms\Classes\Router');
$property = $class->getProperty($name);
$property->setAccessible(true);
return $property;
}
public function testLoadUrlMap()
{
$method = self::getMethod('loadUrlMap');
$property = self::getProperty('urlMap');
$router = new Router(self::$theme);
/*
* The first time the map should be loaded from the disk
*/
$value = $method->invoke($router);
$this->assertFalse($value);
$map = $property->getValue($router);
$this->assertIsArray($map);
$this->assertGreaterThanOrEqual(4, count($map));
/*
* The second time the map should be loaded from the disk
*/
$value = $method->invoke($router);
$this->assertTrue($value);
$map = $property->getValue($router);
$this->assertIsArray($map);
$this->assertGreaterThanOrEqual(4, count($map));
}
public function testUrlListCaching()
{
$router = new Router(self::$theme);
$method = self::getMethod('getCachedUrlFileName');
$urlList = [];
/*
* The first time the page should be loaded from the disk.
*/
$result = $method->invokeArgs($router, ['/', &$urlList]);
$this->assertNull($result);
/*
* Resolve the page to initialize the cache
*/
$page = $router->findByUrl('/');
$this->assertNotEmpty($page);
$this->assertEquals('index.htm', $page->getFileName());
/*
* The second time the page should be loaded from the cache.
*/
$result = $method->invokeArgs($router, ['/', &$urlList]);
$this->assertEquals('index.htm', $result);
/*
* Clear the cache
*/
$router->clearCache();
$result = $method->invokeArgs($router, ['/', &$urlList]);
$this->assertNull($result);
}
public function testFindPageByUrl()
{
$router = new Router(self::$theme);
$page = $router->findByUrl('/');
$this->assertNotEmpty($page);
$this->assertEquals('index.htm', $page->getFileName());
$page = $router->findByUrl('blog/post');
$this->assertEmpty($page);
$page = $router->findByUrl('blog/post/my-post-title');
$parameters = $router->getParameters();
$this->assertNotEmpty($page);
$this->assertEquals('blog-post.htm', $page->getFileName());
$this->assertCount(1, $parameters);
$this->assertArrayHasKey('url_title', $parameters);
$this->assertEquals('my-post-title', $parameters['url_title']);
// Test cached
$page = $router->findByUrl('blog/post/my-post-title');
$parameters = $router->getParameters();
$this->assertNotEmpty($page);
$this->assertEquals('blog-post.htm', $page->getFileName());
$this->assertCount(1, $parameters);
$this->assertArrayHasKey('url_title', $parameters);
$this->assertEquals('my-post-title', $parameters['url_title']);
$page = $router->findByUrl('AuthOrs');
$parameters = $router->getParameters();
$this->assertNotEmpty($page);
$this->assertEquals('authors.htm', $page->getFileName());
$this->assertCount(1, $parameters);
$this->assertArrayHasKey('author_id', $parameters);
$this->assertEquals('no-author', $parameters['author_id']);
$page = $router->findByUrl('AuthOrs/test');
$this->assertEmpty($page);
$page = $router->findByUrl('AuthOrs/test/12');
$this->assertEmpty($page);
$page = $router->findByUrl('AuthOrs/44/');
$parameters = $router->getParameters();
$this->assertNotEmpty($page);
$this->assertEquals('authors.htm', $page->getFileName());
$this->assertCount(1, $parameters);
$this->assertArrayHasKey('author_id', $parameters);
$this->assertEquals('44', $parameters['author_id']);
$page = $router->findByUrl('blog/archive-page');
$parameters = $router->getParameters();
$this->assertNotEmpty($page);
$this->assertEquals('blog-archive.htm', $page->getFileName());
$this->assertCount(1, $parameters);
$page = $router->findByUrl('blog/category-page');
$parameters = $router->getParameters();
$this->assertNotEmpty($page);
$this->assertEquals('blog-category.htm', $page->getFileName());
$this->assertCount(1, $parameters);
$this->assertEquals(array_keys($parameters)[0], 'category_name');
$this->assertEmpty($parameters[array_keys($parameters)[0]]);
$page = $router->findByUrl('blog/category-page/categoryName');
$parameters = $router->getParameters();
$this->assertNotEmpty($page);
$this->assertEquals('blog-category.htm', $page->getFileName());
$this->assertCount(1, $parameters);
$page = $router->findByUrl('blog/category-page/categoryName/subCategoryName');
$parameters = $router->getParameters();
$this->assertNotEmpty($page);
$this->assertEquals('blog-category.htm', $page->getFileName());
$this->assertCount(1, $parameters);
}
public function testFindPageFromSubdirectory()
{
$router = new Router(self::$theme);
$page = $router->findByUrl('/apage');
$this->assertNotEmpty($page);
$this->assertEquals('a/a-page.htm', $page->getFileName());
$page = $router->findByUrl('/bpage');
$this->assertNotEmpty($page);
$this->assertEquals('b/b-page.htm', $page->getFileName());
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Cms\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\Theme;
use Cms\Models\ThemeData;
use Config;
use Event;
use Winter\Storm\Exception\ApplicationException;
class ThemeTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
Config::set('cms.activeTheme', 'test');
Config::set('cms.themesPath', '/modules/cms/tests/fixtures/themes');
Event::flush('cms.theme.getActiveTheme');
Theme::resetCache();
}
protected function countThemePages($path)
{
$result = 0;
$it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));
$it->setMaxDepth(1);
$it->rewind();
while ($it->valid()) {
if (!$it->isDot() && !$it->isDir() && $it->getExtension() == 'htm') {
$result++;
}
$it->next();
}
return $result;
}
public function testGetPath()
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestIncomplete('Need to fix Windows testing here');
}
$theme = Theme::load('test');
$this->assertEquals(base_path('modules/cms/tests/fixtures/themes/test'), $theme->getPath());
}
public function testListPages()
{
$theme = Theme::load('test');
$pageCollection = $theme->listPages();
$pages = array_values($pageCollection->all());
$this->assertIsArray($pages);
$expectedPageNum = $this->countThemePages(base_path() . '/modules/cms/tests/fixtures/themes/test/pages');
$this->assertCount($expectedPageNum, $pages);
$this->assertInstanceOf('\Cms\Classes\Page', $pages[0]);
$this->assertNotEmpty($pages[0]->url);
$this->assertInstanceOf('\Cms\Classes\Page', $pages[1]);
$this->assertNotEmpty($pages[1]->url);
}
public function testGetActiveTheme()
{
$activeTheme = Theme::getActiveTheme();
$this->assertNotNull($activeTheme);
$this->assertEquals('test', $activeTheme->getDirName());
}
public function testNoActiveTheme()
{
$this->expectException(\Winter\Storm\Exception\SystemException::class);
$this->expectExceptionMessage('The active theme is not set.');
Config::set('cms.activeTheme', null);
Theme::getActiveTheme();
}
public function testApiTheme()
{
Event::flush('cms.theme.getActiveTheme');
Event::listen('cms.theme.getActiveTheme', function () {
return 'apitest';
});
$activeTheme = Theme::getActiveTheme();
$this->assertNotNull($activeTheme);
$this->assertEquals('apitest', $activeTheme->getDirName());
}
public function testChildThemeConfig()
{
Config::set('cms.activeTheme', 'childtest');
$theme = Theme::getActiveTheme();
$config = $theme->getConfig();
$this->assertArrayHasKey('parent', $config);
$this->assertEquals('test', $config['parent']);
}
public function testChildThemeAssetUrl()
{
Config::set('cms.activeTheme', 'childtest');
$theme = Theme::getActiveTheme();
$this->assertStringContainsString(
'modules/cms/tests/fixtures/themes/test/assets/css/style1.css',
$theme->assetUrl('assets/css/style1.css')
);
$this->assertStringContainsString(
'modules/cms/tests/fixtures/themes/childtest/assets/css/style2.css',
$theme->assetUrl('assets/css/style2.css')
);
}
public function dirNameValidityProvider(): array
{
return [
// Valid names
'lowercase' => ['mytheme', true],
'uppercase' => ['MYTHEME', true],
'mixed case' => ['MyTheme', true],
'with digits' => ['theme123', true],
'with hyphens' => ['my-theme', true],
'with underscores' => ['my_theme', true],
'all allowed' => ['My-Theme_123', true],
'single char' => ['a', true],
// Invalid names
'empty string' => ['', false],
'dot' => ['.', false],
'dot dot' => ['..', false],
'path traversal' => ['../etc', false],
'forward slash' => ['foo/bar', false],
'backslash' => ['foo\\bar', false],
'spaces' => ['my theme', false],
'special chars' => ['theme!@#', false],
];
}
/**
* @dataProvider dirNameValidityProvider
*/
public function testIsValidDirName(string $dirName, bool $expected): void
{
$this->assertSame($expected, Theme::isValidDirName($dirName));
}
public function testLoadRejectsPathTraversal()
{
$this->expectException(ApplicationException::class);
Theme::load('../../etc');
}
public function testResetCacheClearsThemeData()
{
$themeData = new ThemeData(['theme' => 'test']);
self::setProtectedProperty($themeData, 'instances', ['test' => $themeData]);
Theme::resetCache();
$this->assertEmpty(self::getProtectedProperty($themeData, 'instances'));
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace Cms\Tests\Classes;
use Cms\Twig\Extension;
use Cms\Classes\Controller;
use System\Classes\Asset\PackageManager;
use System\Tests\Bootstrap\TestCase;
use Winter\Storm\Exception\SystemException;
use Winter\Storm\Support\Facades\File;
class TwigExtensionTest extends TestCase
{
private const VITE_FIXTURE_PACKAGE = 'theme-assettest';
private const VITE_FIXTURE_THEME_PATH = '/modules/system/tests/fixtures/themes/assettest';
private const VITE_HOT_URL = 'http://localhost:5173';
public function tearDown(): void
{
$hotFile = base_path(self::VITE_FIXTURE_THEME_PATH . '/assets/dist/hot');
if (File::exists($hotFile)) {
File::delete($hotFile);
}
$distDir = base_path(self::VITE_FIXTURE_THEME_PATH . '/assets/dist');
if (File::isDirectory($distDir) && count(File::files($distDir)) === 0 && count(File::directories($distDir)) === 0) {
File::deleteDirectory($distDir);
}
parent::tearDown();
}
public function testPartialFunction()
{
$extension = new Extension;
$controller = Controller::getController() ?: new Controller;
$extension->setController($controller);
$this->assertFalse($extension->partialFunction('invalid-partial-file', [], false));
$this->expectException(SystemException::class);
$this->expectExceptionMessageMatches('/is\snot\sfound/');
$this->assertFalse($extension->partialFunction('invalid-partial-file', [], true));
}
public function testContentFunction()
{
$extension = new Extension;
$controller = Controller::getController() ?: new Controller;
$extension->setController($controller);
$this->assertFalse($extension->contentFunction('invalid-content-file', [], false));
$this->expectException(SystemException::class);
$this->expectExceptionMessageMatches('/is\snot\sfound/');
$this->assertFalse($extension->contentFunction('invalid-content-file', [], true));
}
public function testStylesTagEmitsCssAndViteCss(): void
{
[$extension, $controller] = $this->buildExtensionWithViteAssets([
'assets/css/theme.css',
'assets/javascript/theme.js',
]);
$controller->addCss('plain.css');
// This is exactly the call StylesNode::compile() writes into the compiled `{% styles %}` tag
$output = (string) $extension->assetsFunction('css');
$this->assertStringContainsString('plain.css', $output);
$this->assertStringContainsString('assets/css/theme.css', $output);
$this->assertStringNotContainsString('assets/javascript/theme.js', $output);
}
public function testScriptsTagEmitsJsAndViteJs(): void
{
[$extension, $controller] = $this->buildExtensionWithViteAssets([
'assets/css/theme.css',
'assets/javascript/theme.js',
]);
$controller->addJs('plain.js');
// Mirrors what ScriptsNode::compile() emits for the `{% scripts %}` tag
$output = (string) $extension->assetsFunction('js');
$this->assertStringContainsString('plain.js', $output);
$this->assertStringContainsString('assets/javascript/theme.js', $output);
$this->assertStringNotContainsString('assets/css/theme.css', $output);
}
public function testStylesAndScriptsTagsDoNotCrossLeak(): void
{
// Reverse entrypoint order to confirm filtering doesn't depend on array order
[$extension] = $this->buildExtensionWithViteAssets([
'assets/javascript/theme.js',
'assets/css/theme.css',
]);
$stylesOutput = (string) $extension->assetsFunction('css');
$scriptsOutput = (string) $extension->assetsFunction('js');
$this->assertStringContainsString('assets/css/theme.css', $stylesOutput);
$this->assertStringNotContainsString('assets/javascript/theme.js', $stylesOutput);
$this->assertStringContainsString('assets/javascript/theme.js', $scriptsOutput);
$this->assertStringNotContainsString('assets/css/theme.css', $scriptsOutput);
}
public function testStylesTagOmitsViteWhenNoCssEntrypoints(): void
{
// JS-only vite registration; the styles tag must contribute no vite output for it
[$extension] = $this->buildExtensionWithViteAssets([
'assets/javascript/theme.js',
]);
$output = (string) $extension->assetsFunction('css');
$this->assertStringNotContainsString('@vite/client', $output);
$this->assertStringNotContainsString('assets/javascript/theme.js', $output);
}
/**
* Boots a Cms\Twig\Extension wired up to a fresh Controller that has the
* given vite entrypoints registered against the assettest fixture package.
* The fixture's hot file is written so Vite::tags() emits deterministic
* dev-server tags (no manifest required).
*
* @return array{0: Extension, 1: Controller}
*/
private function buildExtensionWithViteAssets(array $entrypoints): array
{
$themePath = base_path(self::VITE_FIXTURE_THEME_PATH);
if (!File::isDirectory($themePath)) {
$this->markTestSkipped('Vite test fixture is missing at ' . self::VITE_FIXTURE_THEME_PATH);
}
// PackageManager's lazy init() touches Theme::all() and Halcyon model events, which
// can blow up when prior tests in the full suite have left datasource/plugin state in
// an inconsistent shape. Skip gracefully — same pattern as the existing
// ViteInstallTest — so this test still asserts something useful in isolation.
try {
$packageManager = PackageManager::instance();
} catch (\Throwable $e) {
$this->markTestSkipped('PackageManager could not initialise in this environment: ' . $e->getMessage());
}
// registerPackage silently no-ops when re-registering the same name + config.
$packageManager->registerPackage(
self::VITE_FIXTURE_PACKAGE,
$themePath . '/vite.config.mjs',
'vite'
);
File::ensureDirectoryExists($themePath . '/assets/dist');
File::put($themePath . '/assets/dist/hot', self::VITE_HOT_URL);
$controller = new Controller();
$controller->addVite($entrypoints, self::VITE_FIXTURE_PACKAGE);
$extension = new Extension();
$extension->setController($controller);
return [$extension, $controller];
}
}