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,26 @@
<?php
namespace System\Tests;
use System\Tests\Bootstrap\PluginTestCase;
class AliasesTest extends PluginTestCase
{
public function testInputFacadeAlias()
{
$this->assertTrue(class_exists('Illuminate\Support\Facades\Input'));
$this->assertInstanceOf(
\Winter\Storm\Support\Facades\Input::class,
new \Illuminate\Support\Facades\Input()
);
}
public function testHtmlDumperAlias()
{
$this->assertTrue(class_exists('Illuminate\Support\Debug\HtmlDumper'));
$this->assertInstanceOf(
\Symfony\Component\VarDumper\Dumper\HtmlDumper::class,
new \Illuminate\Support\Debug\HtmlDumper()
);
}
}

View File

@@ -0,0 +1,110 @@
# Plugin testing
Individual plugin test cases can be run by running either `artisan winter:test -p MyAuthor.MyPlugin` in the project root or `../../../vendor/bin/phpunit` in the plugin's base directory (ex. `plugins/acme/demo`).
### Creating plugin tests
Plugins can be tested by creating a file called `phpunit.xml` in the base directory with the following content, for example, in a file **/plugins/acme/blog/phpunit.xml**:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="../../../modules/system/tests/bootstrap/app.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
>
<testsuites>
<testsuite name="MyAuthor.MyPlugin Unit Test Suite">
<directory>./tests</directory>
</testsuite>
</testsuites>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
</php>
</phpunit>
Then a **tests/** directory can be created to contain the test classes. The file structure should mimic the base directory with classes having a `Test` suffix. Using a namespace for the class is also recommended.
```php
namespace Acme\Blog\Tests\Models;
use Acme\Blog\Models\Post;
use System\Tests\Bootstrap\PluginTestCase;
class PostTest extends PluginTestCase
{
public function testCreateFirstPost()
{
$post = Post::create(['title' => 'Hi!']);
$this->assertEquals(1, $post->id);
}
}
```
The test class should extend the base class `System\Tests\Boostrap\PluginTestCase` and this is a special class that will set up the Winter database stored in memory, as part of the `setUp` method. It will also refresh the plugin being tested, along with any of the defined dependencies in the plugin registration file. This is the equivalent of running the following before each test:
```bash
php artisan winter:up
php artisan plugin:refresh Acme.Blog
[php artisan plugin:refresh <dependency>, ...]
```
> **Note:** If your plugin uses [configuration files](../plugin/settings#file-configuration), then you will need to run `System\Classes\PluginManager::instance()->registerAll(true);` in the `setUp` method of your tests. Below is an example of a base test case class that should be used if you need to test your plugin working with other plugins instead of in isolation.
```php
use System\Classes\PluginManager;
use System\Tests\Bootstrap\PluginTestCase;
class BaseTestCase extends PluginTestCase
{
public function setUp(): void
{
parent::setUp();
// Get the plugin manager
$pluginManager = PluginManager::instance();
// Register the plugins to make features like file configuration available
$pluginManager->registerAll(true);
// Boot all the plugins to test with dependencies of this plugin
$pluginManager->bootAll(true);
}
public function tearDown(): void
{
parent::tearDown();
// Get the plugin manager
$pluginManager = PluginManager::instance();
// Ensure that plugins are registered again for the next test
$pluginManager->unregisterAll();
}
}
```
#### Changing database engine for plugins tests
By default Winter CMS uses SQLite stored in memory for the plugin testing environment. If you want to override the default behavior you can override the `/config/database.php` file by creating `/config/testing/database.php`. In this case, variables from the latter file will be taken.
## Winter core modules testing
To perform unit testing on the core Winter modules run `artisan winter:test -m system -m backend -m cms`.
### Unit tests
Unit tests can be performed by running `vendor/bin/phpunit` in the root directory of your Winter CMS installation.
### Functional tests
Functional tests can be performed by installing the [Winter.Dusk](https://wintercms.com/plugin/winter-dusk) in your Winter CMS installation. The Winter.Dusk plugin is powered by Laravel Dusk, a comprehensive testing suite for the Laravel framework that is designed to test interactions with a fully operational Winter CMS instance through a virtual browser.
For information on installing and setting up your Winter CMS install to run functional tests, please review the [README](https://github.com/wintercms/wn-dusk-plugin/blob/master/README.md) for the plugin.

View File

@@ -0,0 +1,28 @@
<?php
namespace System\Tests;
use Db;
use Log;
use System\Tests\Bootstrap\PluginTestCase;
class ServiceProviderTest extends PluginTestCase
{
/**
* Test the registerLogging method
*
* @return void
*/
public function testRegisterLogging()
{
// Verify that calling the Log::info() method and passing in details stores those details in the event log table
$message = 'This is a test log message';
$details = [
'key' => 'Dummy value',
];
Log::info($message, $details);
$latestLog = Db::table('system_event_logs')->latest()->first();
$this->assertEquals($message, $latestLog->message);
$this->assertEquals($details, json_decode($latestLog->details, true));
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace System\Tests\Bootstrap;
use System\Classes\UpdateManager;
use System\Classes\VersionManager;
use System\Classes\PluginManager;
use ReflectionClass;
use Winter\Storm\Database\Model as ActiveRecord;
class PluginManagerTestCase extends TestCase
{
public ?PluginManager $manager = null;
protected $output;
/**
* Creates the application.
* @return Symfony\Component\HttpKernel\HttpKernelInterface
*/
public function createApplication()
{
$app = parent::createApplication();
/*
* Store database in memory by default unless specified otherwise
*/
if (!file_exists(base_path('config/testing/database.php'))) {
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
]);
$app['config']->set('database.default', 'testing');
}
return $app;
}
/**
* Perform test case set up.
* @return void
*/
public function setUp() : void
{
/*
* Force reload of Winter singletons
*/
PluginManager::forgetInstance();
UpdateManager::forgetInstance();
// Forces plugin migrations to be run again on every test
VersionManager::forgetInstance();
$this->output = new \Symfony\Component\Console\Output\BufferedOutput();
parent::setUp();
/*
* Ensure system is up to date
*/
$this->runWinterUpCommand();
$manager = PluginManager::instance();
self::callProtectedMethod($manager, 'init');
$this->manager = $manager;
}
/**
* Flush event listeners and collect garbage.
* @return void
*/
public function tearDown() : void
{
$this->flushModelEventListeners();
parent::tearDown();
unset($this->app);
}
/**
* Migrate database using winter:up command.
* @return void
*/
protected function runWinterUpCommand()
{
UpdateManager::instance()
->setNotesOutput($this->output)
->update();
}
/**
* The models in Winter use a static property to store their events, these
* will need to be targeted and reset ready for a new test cycle.
* Pivot models are an exception since they are internally managed.
* @return void
*/
protected function flushModelEventListeners()
{
foreach (get_declared_classes() as $class) {
if ($class === 'Winter\Storm\Database\Pivot' || strtolower($class) === 'october\rain\database\pivot') {
continue;
}
$reflectClass = new ReflectionClass($class);
if (
!$reflectClass->isInstantiable() ||
!$reflectClass->isSubclassOf('Winter\Storm\Database\Model') ||
$reflectClass->isSubclassOf('Winter\Storm\Database\Pivot')
) {
continue;
}
$class::flushEventListeners();
}
ActiveRecord::flushEventListeners();
}
}

View File

@@ -0,0 +1,261 @@
<?php
namespace System\Tests\Bootstrap;
use Mail;
use Config;
use Artisan;
use Exception;
use ReflectionClass;
use Backend\Classes\AuthManager;
use Backend\Tests\Concerns\InteractsWithAuthentication;
use Mockery\MockInterface;
use System\Classes\PluginBase;
use System\Classes\PluginManager;
use System\Classes\UpdateManager;
use Winter\Storm\Database\Model as BaseModel;
/**
* Plugin test case.
*
* The base test case that should be used for plugin tests. It instantiates the given plugin and
* its dependencies, and ensures that the plugin is available for use within the tests.
*
* @package winter/wn-system-module
*/
abstract class PluginTestCase extends TestCase
{
use InteractsWithAuthentication;
/**
* @var array Cache for storing which plugins have been loaded.
*/
protected $pluginTestCaseLoadedPlugins = [];
/**
* Creates the application.
*
* @return \Symfony\Component\HttpKernel\HttpKernelInterface
*/
public function createApplication()
{
$app = require __DIR__ . '/../../../../bootstrap/app.php';
$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
$app['cache']->setDefaultDriver('array');
$app->setLocale('en');
$app->singleton('backend.auth', function ($app) {
$app['auth.loaded'] = true;
return AuthManager::instance();
});
// Store database in memory by default unless otherwise specified
if (!file_exists(base_path('config/testing/database.php'))) {
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
]);
$app['config']->set('database.default', 'testing');
}
// Set random encryption key
$app['config']->set('app.key', bin2hex(random_bytes(16)));
// Modify the plugin path away from the test context
$app->setPluginsPath(realpath(base_path() . Config::get('cms.pluginsPath')));
return $app;
}
/**
* Perform test case set up.
*/
public function setUp(): void
{
// Reload the plugin and update manager singletons
PluginManager::forgetInstance();
UpdateManager::forgetInstance();
parent::setUp();
// Reset loaded routes
$this->app['router']->setRoutes(new \Illuminate\Routing\RouteCollection);
// Run all migrations
Artisan::call('winter:up');
// Reset loaded plugins for this test
$this->pluginTestCaseLoadedPlugins = [];
// Detect the plugin being tested and load it automatically
$pluginCode = $this->guessPluginCode();
if (!is_null($pluginCode)) {
$this->instantiatePlugin($pluginCode, false);
}
// Reload module routes
foreach (Config::get('cms.loadModules', []) as $module) {
include base_path("modules/" . strtolower($module) . "/routes.php");
}
// Disable mailing
Mail::pretend();
}
/**
* Flush event listeners and tear down.
*/
public function tearDown(): void
{
$this->flushModelEventListeners();
parent::tearDown();
unset($this->app);
}
/**
* Refreshes a plugin for testing.
*
* Since the test environment has loaded all the test plugins natively, this method will ensure
* the desired plugin is loaded in the system before proceeding to migrate it.
*
* @deprecated v1.2.1 Use `instantiatePlugin()` instead.
* @return void
*/
protected function runPluginRefreshCommand($code, $throwException = true)
{
$this->instantiatePlugin((string) $code, (bool) $throwException);
}
/**
* Instantiates a plugin for testing.
*
* @param string $code Plugin code.
* @param boolean $throw Throw an exception if the plugin cannot be found.
*/
protected function instantiatePlugin(string $code, bool $throw = true): void
{
// Check plugin code is valid
if (!preg_match('/^[\w+]*\.[\w+]*$/', $code)) {
if (!$throw) {
return;
}
throw new Exception(sprintf('Invalid plugin code: "%s"', $code));
}
$manager = PluginManager::instance();
$plugin = $manager->findByIdentifier($code);
$firstLoad = !$plugin;
// First time seeing this plugin, load it up
if ($firstLoad) {
$namespace = '\\'.str_replace('.', '\\', $code);
$path = array_get(
array_change_key_case($manager->getPluginNamespaces(), CASE_LOWER),
strtolower($namespace)
);
if (!$path) {
if (!$throw) {
return;
}
throw new Exception(sprintf('Unable to find plugin with code: "%s"', $code));
}
$plugin = $manager->loadPlugin($namespace, $path);
$manager->registerPlugin($plugin);
}
$this->pluginTestCaseLoadedPlugins[$code] = $plugin;
// Load any dependencies
if (!empty($plugin->require)) {
foreach ((array) $plugin->require as $dependency) {
if (isset($this->pluginTestCaseLoadedPlugins[$dependency])) {
continue;
}
$this->instantiatePlugin($dependency);
}
}
// Refresh the plugin's tables
Artisan::call('plugin:refresh', ['plugin' => $code, '--force' => true]);
// Boot the plugin if this is the first load
if ($firstLoad) {
$manager->bootPlugin($plugin);
}
}
/**
* Returns a plugin object from its code, useful for registering events, etc.
*/
protected function getPluginObject($code = null): ?PluginBase
{
return $this->pluginTestCaseLoadedPlugins[$code]
?? $this->pluginTestCaseLoadedPlugins[$this->guessPluginCode()]
?? null;
}
/**
* Flush model event listeners.
*
* The models in Winter use a static property to store their events. These will need to be
* targeted and reset, ready for a new test cycle.
*
* Pivot models are an exception since they are internally managed.
*/
protected function flushModelEventListeners(): void
{
foreach (get_declared_classes() as $class) {
if ($class === 'Winter\Storm\Database\Pivot' || strtolower($class) === 'october\rain\database\pivot') {
continue;
}
$reflectClass = new ReflectionClass($class);
if (
!$reflectClass->isInstantiable() ||
!$reflectClass->isSubclassOf('Winter\Storm\Database\Model') ||
$reflectClass->isSubclassOf('Winter\Storm\Database\Pivot') ||
in_array(MockInterface::class, $reflectClass->getInterfaceNames())
) {
continue;
}
$class::flushEventListeners();
}
BaseModel::flushEventListeners();
}
/**
* Guesses the plugin code being tested.
*/
protected function guessPluginCode(): ?string
{
$reflect = new ReflectionClass($this);
$fqClass = $reflect->getName();
$namespace = $reflect->getNamespaceName();
if (empty($namespace)) {
// Try to determine from the path instead
$path = $reflect->getFilename();
$basePath = $this->app->pluginsPath();
if (!strpos($path, $basePath) === 0) {
return null;
}
$pluginCode = ltrim(str_replace('\\', '/', substr($path, strlen($basePath))), '/');
$pluginCode = implode('.', array_slice(explode('/', $pluginCode), 0, 2));
} else {
// Determine code from namespace
$manager = PluginManager::instance();
$pluginCode = $manager->getIdentifier($fqClass);
}
return $pluginCode;
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace System\Tests\Bootstrap;
use ReflectionClass;
use PHPUnit\Framework\Assert;
class TestCase extends \Illuminate\Foundation\Testing\TestCase
{
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__ . '/../../../../bootstrap/app.php';
$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
$app['cache']->setDefaultDriver('array');
$app->setLocale('en');
// Set random encryption key
$app['config']->set('app.key', bin2hex(random_bytes(16)));
return $app;
}
//
// Helpers
//
protected static function callProtectedMethod($object, $name, $params = [])
{
$className = get_class($object);
$class = new ReflectionClass($className);
$method = $class->getMethod($name);
$method->setAccessible(true);
return $method->invokeArgs($object, $params);
}
public static function getProtectedProperty($object, $name)
{
$className = get_class($object);
$class = new ReflectionClass($className);
$property = $class->getProperty($name);
$property->setAccessible(true);
return $property->getValue($object);
}
public static function setProtectedProperty($object, $name, $value)
{
$className = get_class($object);
$class = new ReflectionClass($className);
$property = $class->getProperty($name);
$property->setAccessible(true);
return $property->setValue($object, $value);
}
/**
* Stub for `assertFileNotExists` to allow compatibility with both PHPUnit 8 and 9.
*
* @param string $filename
* @param string $message
* @return void
*/
public static function assertFileNotExists(string $filename, string $message = ''): void
{
if (method_exists(Assert::class, 'assertFileDoesNotExist')) {
Assert::assertFileDoesNotExist($filename, $message);
return;
}
Assert::assertFileNotExists($filename, $message);
}
/**
* Stub for `assertRegExp` to allow compatibility with both PHPUnit 8 and 9.
*
* @param string $filename
* @param string $message
* @return void
*/
public static function assertRegExp(string $pattern, string $string, string $message = ''): void
{
if (method_exists(Assert::class, 'assertMatchesRegularExpression')) {
Assert::assertMatchesRegularExpression($pattern, $string, $message);
return;
}
Assert::assertRegExp($pattern, $string, $message);
}
}

View File

@@ -0,0 +1,59 @@
<?php
$baseDir = realpath(__DIR__ . '/../../../..');
/*
* Winter autoloader
*/
require $baseDir . '/bootstrap/autoload.php';
/*
* Fallback autoloader
*/
$loader = new Winter\Storm\Support\ClassLoader(
new Winter\Storm\Filesystem\Filesystem,
$baseDir,
$baseDir . '/storage/framework/classes.php'
);
$loader->register();
/*
* Manually register all module classes for autoloading
*/
foreach (glob($baseDir . '/modules/*', GLOB_ONLYDIR) as $modulePath) {
$loader->autoloadPackage(basename($modulePath), $modulePath);
}
/*
* Manually register System aliases
*/
foreach (require(__DIR__ . '/../../aliases.php') as $alias => $class) {
if (!class_exists($alias)) {
class_alias($class, $alias);
}
}
/*
* Manually register all plugin classes for autoloading
*/
$dirPath = $baseDir . '/plugins';
if (is_dir($dirPath)) {
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dirPath, FilesystemIterator::FOLLOW_SYMLINKS)
);
$it->setMaxDepth(2);
$it->rewind();
while ($it->valid()) {
if (($it->getDepth() > 1) && $it->isFile() && (strtolower($it->getFilename()) === "plugin.php")) {
$filePath = dirname($it->getPathname());
$pluginName = basename($filePath);
$vendorName = basename(dirname($filePath));
$loader->autoloadPackage($vendorName . '\\' . $pluginName, $filePath);
}
$it->next();
}
}

View File

@@ -0,0 +1,297 @@
<?php
namespace System\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\Theme;
use System\Classes\CombineAssets;
class CombineAssetsTest extends TestCase
{
public function setUp() : void
{
parent::setUp();
CombineAssets::resetCache();
}
//
// Tests
//
public function testCombiner()
{
$combiner = CombineAssets::instance();
/*
* Supported file extensions should exist
*/
$jsExt = $cssExt = self::getProtectedProperty($combiner, 'jsExtensions');
$this->assertIsArray($jsExt);
$cssExt = self::getProtectedProperty($combiner, 'cssExtensions');
$this->assertIsArray($cssExt);
/*
* Check service methods
*/
$this->assertTrue(method_exists($combiner, 'combine'));
$this->assertTrue(method_exists($combiner, 'resetCache'));
}
public function testCombine()
{
$combiner = CombineAssets::instance();
$url = $combiner->combine(
[
'assets/css/style1.css',
'assets/css/style2.css'
],
base_path() . '/modules/system/tests/fixtures/themes/test'
);
$this->assertNotNull($url);
$this->assertRegExp('/\w+[-]\d+/i', $url); // Must contain hash-number
$url = $combiner->combine(
[
'assets/js/script1.js',
'assets/js/script2.js'
],
base_path() . '/modules/system/tests/fixtures/themes/test'
);
$this->assertNotNull($url);
$this->assertRegExp('/\w+[-]\d+/i', $url); // Must contain hash-number
}
public function testPutCache()
{
$sampleId = md5('testhash');
$sampleStore = ['version' => 12345678];
$samplePath = '/tests/fixtures/Cms/themes/test';
$combiner = CombineAssets::instance();
$value = self::callProtectedMethod($combiner, 'putCache', [$sampleId, $sampleStore]);
$this->assertTrue($value);
}
public function testGetTargetPath()
{
$combiner = CombineAssets::instance();
$value = self::callProtectedMethod($combiner, 'getTargetPath', ['/combine']);
$this->assertEquals('combine/', $value);
$value = self::callProtectedMethod($combiner, 'getTargetPath', ['/index.php/combine']);
$this->assertEquals('index-php/combine/', $value);
}
public function testMakeCacheId()
{
$sampleResources = ['assets/css/style1.css', 'assets/css/style2.css'];
$samplePath = base_path() . '/modules/system/tests/fixtures/cms/themes/test';
$combiner = CombineAssets::instance();
self::setProtectedProperty($combiner, 'localPath', $samplePath);
$value = self::callProtectedMethod($combiner, 'getCacheKey', [$sampleResources]);
$this->assertEquals(md5($samplePath.implode('|', $sampleResources)), $value);
}
public function testResetCache()
{
$combiner = CombineAssets::instance();
$this->assertNull($combiner->resetCache());
}
/**
* Regression for GHSA-58fp-mcx6-7qf9. A writable theme `.less` file containing
* `@import (inline) "<absolute-path>"` must not disclose server files.
*/
public function testLessCompilerBlocksAbsolutePathImport()
{
[$themeDir, $secretPath] = $this->setupLessLeakFixture(
'@import (inline) "%SECRET%"; .x { color: red; }'
);
try {
$css = $this->compileLessTo($themeDir, 'assets/less/poc.less');
$this->assertStringNotContainsString('APP_KEY', $css);
$this->assertStringNotContainsString('combine-leak-canary', $css);
} finally {
$this->teardownLessLeakFixture($themeDir, $secretPath);
}
}
/**
* Regression for the relative-traversal path of GHSA-58fp-mcx6-7qf9. less.php's
* auto-added `currentDirectory` import_dir entry resolves `..` traversal
* natively; without the key-collision override in LessImportResolver, a theme
* `.less` could still escape via `@import (inline) "../../../etc/passwd"`.
*/
public function testLessCompilerBlocksRelativeTraversalImport()
{
// From themeDir/assets/less/poc.less, traverse up enough to escape the
// theme tree, the themes root, and out to the secret file the fixture
// wrote at sys_get_temp_dir().
[$themeDir, $secretPath] = $this->setupLessLeakFixture(
'@import (inline) "' . str_repeat('../', 20) . ltrim($this->lastSecretPath, '/') . '"; .x { color: red; }'
);
try {
$css = $this->compileLessTo($themeDir, 'assets/less/poc.less');
$this->assertStringNotContainsString('APP_KEY', $css);
$this->assertStringNotContainsString('combine-leak-canary', $css);
} finally {
$this->teardownLessLeakFixture($themeDir, $secretPath);
}
}
/**
* Legitimate same-tree `@import "partial.less"` must still resolve through
* the gate, otherwise we've broken every theme that uses partials.
*/
public function testLessCompilerAllowsLegitimatePartial()
{
$themeDir = $this->makeTempThemeDir();
$mainPath = $themeDir . '/assets/less/main.less';
$partialPath = $themeDir . '/assets/less/partial.less';
file_put_contents($partialPath, '.partial-marker { color: orange; }');
file_put_contents($mainPath, '@import "partial.less"; .main-marker { color: blue; }');
try {
$css = $this->compileLessTo($themeDir, 'assets/less/main.less');
$this->assertStringContainsString('partial-marker', $css);
$this->assertStringContainsString('main-marker', $css);
} finally {
\File::deleteDirectory($themeDir);
}
}
/**
* Regression for GHSA-2223-f22x-24cq. A writable theme `.js` asset containing
* `=include ../../../.env` must not disclose server files through the combiner,
* whose output is served unauthenticated via the `combine/{file}` route.
*/
public function testJavascriptImporterBlocksTraversalImport()
{
$themeDir = $this->makeTempThemeDir();
// A real `.js` secret outside the theme subtree (but under base_path so
// Assetic's FileAsset root check passes). It escapes via `..` traversal but
// lands outside every allowed import root, so it must not be inlined.
$secretPath = dirname($themeDir) . '/js-secret-' . bin2hex(random_bytes(4)) . '.js';
file_put_contents($secretPath, 'var LEAK = "combine-leak-canary";');
file_put_contents(
$themeDir . '/assets/poc.js',
"/*\n=include ../../" . basename($secretPath) . "\n*/\n"
);
try {
$js = $this->compileJsTo($themeDir, 'assets/poc.js');
$this->assertStringNotContainsString('combine-leak-canary', $js);
} finally {
@unlink($secretPath);
\File::deleteDirectory($themeDir);
}
}
/**
* The `.js`-only extension gate must block disclosure of non-JS files (e.g.
* `.env`) before any path resolution, even inside an otherwise reachable tree.
*/
public function testJavascriptImporterBlocksDisallowedExtension()
{
$themeDir = $this->makeTempThemeDir();
$secretPath = dirname($themeDir) . '/js-secret-' . bin2hex(random_bytes(4)) . '.env';
file_put_contents($secretPath, "APP_KEY=combine-leak-canary\n");
file_put_contents(
$themeDir . '/assets/poc.js',
"/*\n=include ../../" . basename($secretPath) . "\n*/\n"
);
try {
$js = $this->compileJsTo($themeDir, 'assets/poc.js');
$this->assertStringNotContainsString('combine-leak-canary', $js);
} finally {
@unlink($secretPath);
\File::deleteDirectory($themeDir);
}
}
/**
* Legitimate same-tree `=include partial.js` must still resolve, otherwise the
* hardening would break every asset that composes its own bundle.
*/
public function testJavascriptImporterAllowsSameTreeInclude()
{
$themeDir = $this->makeTempThemeDir();
file_put_contents($themeDir . '/assets/partial.js', 'var PARTIAL = "partial-marker";');
file_put_contents($themeDir . '/assets/main.js', "/*\n=include partial.js\n*/\nvar MAIN = 1;");
try {
$js = $this->compileJsTo($themeDir, 'assets/main.js');
$this->assertStringContainsString('partial-marker', $js);
} finally {
\File::deleteDirectory($themeDir);
}
}
protected function compileJsTo(string $themeDir, string $relativeAsset): string
{
$dest = sys_get_temp_dir() . '/winter-combine-out-' . bin2hex(random_bytes(4)) . '.js';
try {
CombineAssets::instance()->combineToFile([$relativeAsset], $dest, $themeDir);
return file_get_contents($dest) ?: '';
} finally {
@unlink($dest);
}
}
/** @var string */
protected $lastSecretPath = '';
/**
* @return array{0:string,1:string} [theme dir, secret path]
*/
protected function setupLessLeakFixture(string $pocTemplate): array
{
$themeDir = $this->makeTempThemeDir();
$secretPath = tempnam(sys_get_temp_dir(), 'combine-leak-canary-');
file_put_contents($secretPath, "APP_KEY=do-not-leak-via-combiner\n");
$this->lastSecretPath = $secretPath;
$poc = str_replace('%SECRET%', $secretPath, $pocTemplate);
file_put_contents($themeDir . '/assets/less/poc.less', $poc);
return [$themeDir, $secretPath];
}
protected function teardownLessLeakFixture(string $themeDir, string $secretPath): void
{
@unlink($secretPath);
\File::deleteDirectory($themeDir);
}
protected function makeTempThemeDir(): string
{
// Must live under base_path() because Assetic's FileAsset enforces that
// the source be within the configured root, which CombineAssets sets to
// public_path() (equal to base_path() in this install). Using sys_get_temp_dir()
// would trigger "source is not in the root directory" errors.
$themeDir = base_path('storage/framework/cache/security-tests/theme-' . bin2hex(random_bytes(4)));
mkdir($themeDir . '/assets/less', 0777, true);
return $themeDir;
}
protected function compileLessTo(string $themeDir, string $relativeAsset): string
{
$dest = sys_get_temp_dir() . '/winter-combine-out-' . bin2hex(random_bytes(4)) . '.css';
try {
CombineAssets::instance()->combineToFile([$relativeAsset], $dest, $themeDir);
return file_get_contents($dest) ?: '';
} finally {
@unlink($dest);
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace System\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use System\Classes\PluginManager;
use Validator;
class CoreLangTest extends TestCase
{
public function testValidationTranslator()
{
$translator = $this->app['translator'];
$translator->setLocale('en');
$validator = Validator::make(
['name' => 'me'],
['name' => 'required|min:5']
);
$this->assertTrue($validator->fails());
$messages = $validator->messages();
$this->assertCount(1, $messages);
$this->assertEquals('The name must be at least 5 characters.', $messages->all()[0]);
}
public function testValidCoreLanguageFiles()
{
$translator = $this->app['translator'];
$locales = $translator->get('system::lang.locale');
$this->assertNotEmpty($locales);
$locales = array_keys($locales);
$modules = ['system', 'backend', 'cms'];
$files = ['lang.php', 'validation.php', 'client.php'];
foreach ($modules as $module) {
foreach ($locales as $locale) {
foreach ($files as $file) {
$srcPath = base_path() . '/modules/'.$module.'/lang/'.$locale.'/'.$file;
if (!file_exists($srcPath)) {
continue;
}
$messages = require $srcPath;
$this->assertNotEmpty($messages);
$this->assertNotCount(0, $messages);
}
}
}
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace System\Tests\Classes;
use ReflectionClass;
use System\Tests\Bootstrap\TestCase;
use Winter\Storm\Exception\ApplicationException;
use System\Classes\FileManifest;
class FileManifestTest extends TestCase
{
/** @var FileManifest instance */
protected $fileManifest;
/** @var root path */
protected $root;
public function setUp(): void
{
parent::setUp();
$this->root = base_path('modules/system/tests/fixtures/manifest/1_0_1');
$this->fileManifest = new FileManifest($this->root, ['test', 'test2']);
}
public function testGetFiles()
{
$this->assertEquals([
'/modules/test/file1.php' => '6f9b0b94528a85b2a6bb67b5621e074aef1b4c9fc9ee3ea1bd69100ea14cb3db',
'/modules/test/file2.php' => '96ae9f6b6377ad29226ea169f952de49fc29ae895f18a2caed76aeabdf050f1b',
'/modules/test2/file1.php' => '94bd47b1ac7b2837b31883ebcd38c8101687321f497c3c4b9744f68ae846721d',
], $this->fileManifest->getFiles());
}
public function testGetModuleChecksums()
{
$this->assertEquals([
'test' => 'c0b794ff210862a4ce16223802efe6e28969f5a4fb42480ec8c2fef2da23d181',
'test2' => '32c9f2fb6e0a22dde288a0fe1e4834798360b25e5a91d2597409d9302221381d',
], $this->fileManifest->getModuleChecksums());
}
public function testGetFilesInvalidRoot()
{
$this->expectException(ApplicationException::class);
$this->expectExceptionMessage('Invalid root specified for the file manifest.');
$this->fileManifest->setRoot(base_path('tests/fixtures/manifest/invalid'));
$this->fileManifest->getFiles();
}
public function testSingleModule()
{
$this->fileManifest->setModules(['test']);
$this->assertEquals([
'/modules/test/file1.php' => '6f9b0b94528a85b2a6bb67b5621e074aef1b4c9fc9ee3ea1bd69100ea14cb3db',
'/modules/test/file2.php' => '96ae9f6b6377ad29226ea169f952de49fc29ae895f18a2caed76aeabdf050f1b',
], $this->fileManifest->getFiles());
$this->assertEquals([
'test' => 'c0b794ff210862a4ce16223802efe6e28969f5a4fb42480ec8c2fef2da23d181',
], $this->fileManifest->getModuleChecksums());
}
public function testGetFilename()
{
$class = new ReflectionClass('System\Classes\FileManifest');
$method = $class->getMethod('getFilename');
$method->setAccessible(true);
$filename = '/modules/test/file1.php';
$this->assertEquals($filename, $method->invoke($this->fileManifest, $this->root . $filename));
}
}

View File

@@ -0,0 +1,482 @@
<?php
namespace System\Tests\Classes;
use Backend\Facades\Backend;
use Cms\Classes\Controller as CmsController;
use Cms\Classes\Theme;
use Config;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
use Event;
use Storage;
use System\Classes\ImageResizer;
use System\Classes\MediaLibrary;
use System\Models\File as FileModel;
use System\Tests\Bootstrap\PluginTestCase;
use URL;
use Winter\Storm\Exception\SystemException;
class ImageResizerTest extends PluginTestCase
{
use ArraySubsetAsserts;
protected $originalThemesPath = '';
public function setUp(): void
{
parent::setUp();
$this->originalThemesPath = Config::get('cms.themesPath');
Config::set('cms.themesPath', '/modules/system/tests/fixtures/themes');
Config::set('cms.activeTheme', 'test');
Event::flush('cms.theme.getActiveTheme');
Theme::resetCache();
}
public function tearDown(): void
{
$this->removeMedia();
Config::set('cms.themesPath', $this->originalThemesPath);
ImageResizer::flushAvailableSources();
parent::tearDown();
}
/**
* Tests configuration through the constructor as well as events.
*
* @return void
*/
public function testConfiguration()
{
if (!in_array('Cms', Config::get('cms.loadModules', []))) {
$this->markTestSkipped('The CMS module is not active.');
}
// Resize with default options
$imageResizer = new ImageResizer(
(new CmsController())->themeUrl('assets/images/winter.png'),
100,
100
);
self::assertArraySubset([
'width' => 100,
'height' => 100,
'options' => [
'mode' => 'auto',
'offset' => [0, 0],
'sharpen' => 0,
'interlace' => false,
'quality' => 90,
'extension' => 'png',
],
], $imageResizer->getConfig());
// Resize with customised options
$imageResizer = new ImageResizer(
(new CmsController())->themeUrl('assets/images/winter.png'),
150,
120,
[
'mode' => 'fit',
'offset' => [2, 2],
'sharpen' => 23,
'interlace' => true,
'quality' => 73,
'extension' => 'jpg'
]
);
self::assertArraySubset([
'width' => 150,
'height' => 120,
'options' => [
'mode' => 'fit',
'offset' => [2, 2],
'sharpen' => 23,
'interlace' => true,
'quality' => 73,
'extension' => 'jpg'
],
], $imageResizer->getConfig());
// Resize with an customised defaults
Event::listen('system.resizer.getDefaultOptions', function (&$options) {
$options = array_merge($options, [
'mode' => 'fit',
'offset' => [2, 2],
'sharpen' => 23,
'interlace' => true,
'quality' => 73,
]);
});
$imageResizer = new ImageResizer(
(new CmsController())->themeUrl('assets/images/winter.png'),
100,
100,
[]
);
self::assertArraySubset([
'width' => 100,
'height' => 100,
'options' => [
'mode' => 'fit',
'offset' => [2, 2],
'sharpen' => 23,
'interlace' => true,
'quality' => 73,
'extension' => 'png',
],
], $imageResizer->getConfig());
Event::forget('system.resizer.getDefaultOptions');
// Resize with a falsey height specified
$imageResizer = new ImageResizer(
(new CmsController())->themeUrl('assets/images/winter.png'),
100,
false
);
self::assertArraySubset([
'width' => 100,
'height' => 0,
], $imageResizer->getConfig());
$imageResizer = new ImageResizer(
(new CmsController())->themeUrl('assets/images/winter.png'),
100,
null
);
self::assertArraySubset([
'width' => 100,
'height' => 0,
], $imageResizer->getConfig());
// Resize with a falsey width specified
$imageResizer = new ImageResizer(
(new CmsController())->themeUrl('assets/images/winter.png'),
'',
100
);
self::assertArraySubset([
'width' => 0,
'height' => 100,
], $imageResizer->getConfig());
$imageResizer = new ImageResizer(
(new CmsController())->themeUrl('assets/images/winter.png'),
"0",
100
);
self::assertArraySubset([
'width' => 0,
'height' => 100,
], $imageResizer->getConfig());
}
/**
* Tests URLs for sources that can be accessed via URL.
*
* @return void
*/
public function testURLSources()
{
if (!in_array('Cms', Config::get('cms.loadModules', []))) {
$this->markTestSkipped('The CMS module is not active.');
}
// Theme URL (absolute URL)
$this->setUpStorage();
$this->copyMedia();
$imageResizer = new ImageResizer(
(new CmsController())->themeUrl('assets/images/winter.png'),
100,
100
);
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
// Theme URL (relative URL)
$this->setUpStorage();
$this->copyMedia();
$imageResizer = new ImageResizer(
'/modules/system/tests/fixtures/themes/test/assets/images/winter.png',
100,
100
);
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
// Media URL (absolute URL)
$this->setUpStorage();
$this->copyMedia();
$imageResizer = new ImageResizer(
URL::to(MediaLibrary::url('winter.png')),
100,
100
);
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
// Media URL (relative URL)
$this->setUpStorage();
$this->copyMedia();
$imageResizer = new ImageResizer(
MediaLibrary::url('winter.png'),
100,
100
);
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
// Media URL (absolute URL)
$this->setUpStorage();
$this->copyMedia();
$imageResizer = new ImageResizer(
URL::to(MediaLibrary::url('winter.png')),
100,
100
);
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
// Plugin URL (relative URL)
$imageResizer = new ImageResizer(
'/modules/system/tests/fixtures/plugins/database/tester/assets/images/avatar.png',
100,
100
);
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
// Plugin URL (absolute URL)
$imageResizer = new ImageResizer(
URL::to('modules/system/tests/fixtures/plugins/database/tester/assets/images/avatar.png'),
100,
100
);
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
// Module URL (relative URL)
$imageResizer = new ImageResizer(
'/modules/backend/assets/images/favicon.png',
100,
100
);
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
// Module URL (absolute URL)
$imageResizer = new ImageResizer(
Backend::skinAsset('assets/images/favicon.png'),
100,
100
);
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
// URL for a FileModel instance (absolute URL)
$fileModel = new FileModel();
$fileModel->fromFile(base_path('modules/system/tests/fixtures/plugins/database/tester/assets/images/avatar.png'));
$fileModel->save();
$imageResizer = new ImageResizer(
FileModel::first()->getPath(),
100,
100
);
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
// Remove FileModel instance
$fileModel->delete();
// URL of a FileModel instance (relative URL)
$fileModel = new FileModel();
$fileModel->fromFile(base_path('modules/system/tests/fixtures/plugins/database/tester/assets/images/avatar.png'));
$fileModel->save();
$imageResizer = new ImageResizer(
str_replace(url('') . '/', '/', FileModel::first()->getPath()),
100,
100
);
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
}
public function testDirectSources()
{
// FileModel instance itself
$fileModel = new FileModel();
$fileModel->fromFile(base_path('modules/system/tests/fixtures/plugins/database/tester/assets/images/avatar.png'));
$fileModel->save();
$imageResizer = new ImageResizer(
$fileModel,
100,
100
);
$this->assertEquals('png', $imageResizer->getConfig()['options']['extension']);
// Remove FileModel instance
$fileModel->delete();
}
public function testInvalidInputPath()
{
$this->expectException(SystemException::class);
$this->expectExceptionMessageMatches('/^Unable to process the provided image/');
$imageResizer = new ImageResizer(
'/plugins/database/tester/assets/images/MISSING.png',
100,
100
);
}
public function testInvalidInputFileModel()
{
$this->expectException(SystemException::class);
$this->expectExceptionMessageMatches('/^Unable to process the provided image/');
$imageResizer = new ImageResizer(
FileModel::first(),
100,
100
);
}
public function testSpaceInFilename()
{
// Media URL with space
$this->setUpStorage();
$this->copyMedia();
$imageResizer = new ImageResizer(
URL::to(MediaLibrary::url('winter space.png')),
100,
100
);
$this->assertStringContainsString('winter%20space', $imageResizer->getResizedUrl(), 'Resized URLs are not properly URL encoded');
}
public function testGetResizedUrl()
{
if (!in_array('Cms', Config::get('cms.loadModules', []))) {
$this->markTestSkipped('The CMS module is not active.');
}
$imageResizer = new ImageResizer((new CmsController())->themeUrl('assets/images/winter.png'));
Config::set('cms.linkPolicy', 'force');
$url = $imageResizer->getResizedUrl();
$this->assertTrue(starts_with($url, 'http'));
Config::set('cms.linkPolicy', 'detect');
$url = $imageResizer->getResizedUrl();
$this->assertTrue(starts_with($url, Config::get('cms.storage.resized.path', '/storage/tests/app/resized')));
}
public function testGetResizerUrl()
{
if (!in_array('Cms', Config::get('cms.loadModules', []))) {
$this->markTestSkipped('The CMS module is not active.');
}
$imageResizer = new ImageResizer((new CmsController())->themeUrl('assets/images/winter.png'));
Config::set('cms.linkPolicy', 'force');
$url = $imageResizer->getResizerUrl();
$this->assertTrue(starts_with($url, 'http'));
Config::set('cms.linkPolicy', 'detect');
$url = $imageResizer->getResizerUrl();
$this->assertTrue(starts_with($url, '/resizer/'));
// test dots' double-encoding
// @see https://github.com/wintercms/winter/pull/1493
$this->assertTrue(ends_with($url, '%252Epng'));
// Verify the encoded URL round-trips through the resizer route's decoding and
// signature verification. A fresh instance is required as the identifier is
// cached on first generation and the link policy has changed since then. The
// router decodes the parameter once before it reaches getValidResizedUrl().
$imageResizer = new ImageResizer((new CmsController())->themeUrl('assets/images/winter.png'));
[$identifier, $encodedUrl] = array_slice(explode('/', $imageResizer->getResizerUrl()), 2);
$this->assertSame(
$imageResizer->getResizedUrl(),
ImageResizer::getValidResizedUrl($identifier, rawurldecode($encodedUrl))
);
}
public function testResizerRedirect()
{
if (!in_array('Cms', Config::get('cms.loadModules', []))) {
$this->markTestSkipped('The CMS module is not active.');
}
$this->setUpStorage();
$this->copyMedia();
Config::set('cms.storage.resized', [
'disk' => 'test_local',
'folder' => 'resized',
'path' => '/storage/temp/app/resized',
]);
$imageResizer = new ImageResizer((new CmsController())->themeUrl('assets/images/winter.png'), 50, 50);
// The resizer route responds with a permanent redirect as a resizer URL can
// only ever target the resized URL embedded and signed within it, and this
// also exercises the full round-trip of the double-encoded URL parameter
// through the actual router
$response = $this->get($imageResizer->getResizerUrl());
$response->assertStatus(301);
$response->assertRedirect($imageResizer->getResizedUrl());
// Clean up the generated image
Storage::disk('test_local')->deleteDirectory('resized');
}
protected function setUpStorage()
{
$this->app->useStoragePath(base_path('storage/temp'));
Config::set('filesystems.disks.test_local', [
'driver' => 'local',
'root' => storage_path('app'),
]);
Config::set('cms.storage.media', [
'disk' => 'test_local',
'folder' => 'media',
'path' => '/storage/temp/app/media',
]);
}
protected function copyMedia()
{
$mediaPath = storage_path('app/media');
if (!is_dir($mediaPath)) {
mkdir($mediaPath, 0777, true);
}
foreach (glob(base_path('modules/system/tests/fixtures/media/*')) as $file) {
$path = pathinfo($file);
copy($file, $mediaPath . DIRECTORY_SEPARATOR . $path['basename']);
}
}
protected function removeMedia()
{
if ($this->app->storagePath() !== base_path('storage/temp')) {
return;
}
foreach (glob(storage_path('app/media/*')) as $file) {
unlink($file);
}
rmdir(storage_path('app/media'));
rmdir(storage_path('app'));
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace System\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use System\Classes\MarkupManager;
class MarkupManagerTest extends TestCase
{
public function setUp() : void
{
parent::setUp();
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/Plugin.php';
}
//
// Tests
//
public function testIsWildCallable()
{
$manager = MarkupManager::instance();
/*
* Negatives
*/
$callable = 'something';
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]);
$this->assertFalse($result);
$callable = ['Form', 'open'];
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]);
$this->assertFalse($result);
$callable = function () {
return 'O, Hai!';
};
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]);
$this->assertFalse($result);
/*
* String
*/
$callable = 'something_*';
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]);
$this->assertTrue($result);
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'delicious']);
$this->assertEquals('something_delicious', $result);
/*
* Array
*/
$callable = ['Class', 'foo_*'];
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]);
$this->assertTrue($result);
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'bar']);
$this->assertArrayHasKey(0, $result);
$this->assertArrayHasKey(1, $result);
$this->assertEquals('Class', $result[0]);
$this->assertEquals('foo_bar', $result[1]);
$callable = ['My*', 'method'];
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]);
$this->assertTrue($result);
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'Class']);
$this->assertArrayHasKey(0, $result);
$this->assertArrayHasKey(1, $result);
$this->assertEquals('MyClass', $result[0]);
$this->assertEquals('method', $result[1]);
$callable = ['My*', 'my*'];
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable]);
$this->assertTrue($result);
$result = self::callProtectedMethod($manager, 'isWildCallable', [$callable, 'Food']);
$this->assertArrayHasKey(0, $result);
$this->assertArrayHasKey(1, $result);
$this->assertEquals('MyFood', $result[0]);
$this->assertEquals('myFood', $result[1]);
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace System\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Illuminate\Filesystem\FilesystemAdapter;
use System\Classes\MediaLibrary;
class MediaLibraryTest extends TestCase
{
public function setUp(): void
{
MediaLibrary::forgetInstance();
parent::setUp();
}
public function tearDown(): void
{
$this->removeMedia();
parent::tearDown();
}
public function invalidPathsProvider()
{
return [
['./file'],
['../secret'],
['.../secret'],
['/../secret'],
['/.../secret'],
['/secret/..'],
['file/../secret'],
['file/..'],
['......./secret'],
['./file'],
];
}
public function validPathsProvider()
{
return [
['file'],
['folder/file'],
['/file'],
['/folder/file'],
['/.file'],
['/..file'],
['/...file'],
['file.ext'],
['file..ext'],
['file...ext'],
['one,two.ext'],
['one(two)[].ext'],
['one=(two)[].ext'],
['one_(two)[].ext'],
/*
Example of a unicode-based filename with a single quote
@see: https://github.com/octobercms/october/pull/4564
*/
['BG中国通讯期刊(Blend\'r)创刊号.pdf'],
];
}
/**
* @dataProvider invalidPathsProvider
*/
public function testInvalidPathsOnValidatePath($path)
{
$this->expectException('ApplicationException');
MediaLibrary::validatePath($path);
}
/**
* @dataProvider validPathsProvider
*/
public function testValidPathsOnValidatePath($path)
{
$result = MediaLibrary::validatePath($path);
$this->assertIsString($result);
}
public function testListFolderContents()
{
$this->setUpStorage();
$this->copyMedia();
$contents = MediaLibrary::instance()->listFolderContents();
$this->assertNotEmpty($contents, 'Media library item is not discovered');
$this->assertCount(3, $contents);
$this->assertEquals('file', $contents[2]->type, 'Media library item does not have the right type');
$this->assertEquals('/winter.png', $contents[2]->path, 'Media library item does not have the right path');
$this->assertNotEmpty($contents[2]->lastModified, 'Media library item last modified is empty');
$this->assertNotEmpty($contents[2]->size, 'Media library item size is empty');
$this->assertEquals('file', $contents[0]->type, 'Media library item does not have the right type');
$this->assertEquals('/text.txt', $contents[0]->path, 'Media library item does not have the right path');
$this->assertNotEmpty($contents[0]->lastModified, 'Media library item last modified is empty');
$this->assertNotEmpty($contents[0]->size, 'Media library item size is empty');
}
public function testListAllDirectories()
{
$disk = $this->createConfiguredMock(FilesystemAdapter::class, [
'allDirectories' => [
'/media/.ignore1',
'/media/.ignore2',
'/media/dir',
'/media/dir/sub',
'/media/exclude',
'/media/hidden',
'/media/hidden/sub1',
'/media/hidden/sub1/deep1',
'/media/hidden/sub2',
'/media/hidden but not really',
'/media/name'
]
]);
$this->app['config']->set('cms.storage.media.folder', 'media');
$this->app['config']->set('cms.storage.media.ignore', ['hidden']);
$this->app['config']->set('cms.storage.media.ignorePatterns', ['^\..*']);
$instance = MediaLibrary::instance();
$this->setProtectedProperty($instance, 'storageDisk', $disk);
$this->assertEquals(['/', '/dir', '/dir/sub', '/hidden but not really', '/name'], $instance->listAllDirectories(['/exclude']));
}
protected function setUpStorage()
{
$this->app->useStoragePath(base_path('storage/temp'));
config(['filesystems.disks.test_local' => [
'driver' => 'local',
'root' => storage_path('app'),
]]);
config(['cms.storage.media' => [
'disk' => 'test_local',
'folder' => 'media',
'path' => '/storage/app/media',
]]);
}
protected function copyMedia()
{
$mediaPath = storage_path('app/media');
if (!is_dir($mediaPath)) {
mkdir($mediaPath, 0777, true);
}
foreach (glob(base_path('modules/system/tests/fixtures/media/*')) as $file) {
$path = pathinfo($file);
copy($file, $mediaPath . DIRECTORY_SEPARATOR . $path['basename']);
}
}
protected function removeMedia()
{
if ($this->app->storagePath() !== base_path('storage/temp')) {
return;
}
foreach (glob(storage_path('app/media/*')) as $file) {
unlink($file);
}
rmdir(storage_path('app/media'));
rmdir(storage_path('app'));
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace System\Tests\Classes;
use System\Tests\Bootstrap\PluginManagerTestCase;
class PluginBaseTest extends PluginManagerTestCase
{
//
// Tests
//
public function testGetPluginVersions()
{
$expectedVersions = [
'1.0.1' => [
'Added some upgrade file and some "seeding"',
'some_seeding_file.php' //does not exist
],
'1.0.2' => [
'Added some stuff',
],
'1.0.3' => [
'Bug fix update that uses no scripts'
],
'1.0.4' => [
'Another fix'
],
'1.0.5' => [
'Create blog settings table',
'Another update message',
'Yet one more update message',
],
'1.1.0' => [
'!!! Drop support for blog settings',
],
'1.2.0' => [
'!!! Security update - see: https://wintercms.com',
],
'1.3.0' => [
'!!! We\'ve refactored major parts of this plugin. Please see the website for more information.',
],
'1.3.1' => [
'Minor bug fix Please see changelog',
],
'1.3.2' => [
'Added support for Translate plugin. Added some new languages.',
],
'1.4.1' => [
'!!! Major update here.',
],
'1.5.0' => [
'!!! Another major update to fix several issues',
],
'1.5.1' => [
'Improved signature with the Test::method()',
'Translation updates.',
],
];
$plugin = $this->manager->findByIdentifier('Winter.Tester');
$versions = $plugin->getPluginVersions(false);
$this->assertNotNull($versions);
$this->assertEquals($expectedVersions, $versions);
}
}

View File

@@ -0,0 +1,498 @@
<?php
namespace System\Tests\Classes;
use System\Tests\Bootstrap\PluginManagerTestCase;
use System\Classes\PluginManager;
use System\Classes\PluginBase;
class PluginManagerTest extends PluginManagerTestCase
{
const INSTALLED_PLUGIN_COUNT = 17;
const ENABLED_PLUGIN_COUNT = 14;
const PLUGIN_NAMESPACE_COUNT = 18;
const PLUGIN_VENDOR_COUNT = 5;
//
// Tests
//
public function testLoadPlugins()
{
$result = $this->manager->loadPlugins();
$this->assertCount(static::INSTALLED_PLUGIN_COUNT, $result);
$this->assertArrayHasKey('Winter.NoUpdates', $result);
$this->assertArrayHasKey('Winter.Sample', $result);
$this->assertArrayHasKey('Winter.Tester', $result);
$this->assertArrayHasKey('Database.Tester', $result);
$this->assertArrayHasKey('TestVendor.Test', $result);
$this->assertArrayHasKey('DependencyTest.Found', $result);
$this->assertArrayHasKey('DependencyTest.NotFound', $result);
$this->assertArrayHasKey('DependencyTest.WrongCase', $result);
$this->assertArrayHasKey('DependencyTest.Dependency', $result);
$this->assertArrayNotHasKey('TestVendor.Goto', $result);
$this->assertInstanceOf('Winter\NoUpdates\Plugin', $result['Winter.NoUpdates']);
$this->assertInstanceOf('Winter\Sample\Plugin', $result['Winter.Sample']);
$this->assertInstanceOf('Winter\Tester\Plugin', $result['Winter.Tester']);
$this->assertInstanceOf('Database\Tester\Plugin', $result['Database.Tester']);
$this->assertInstanceOf('TestVendor\Test\Plugin', $result['TestVendor.Test']);
$this->assertInstanceOf('DependencyTest\Found\Plugin', $result['DependencyTest.Found']);
$this->assertInstanceOf('DependencyTest\NotFound\Plugin', $result['DependencyTest.NotFound']);
$this->assertInstanceOf('DependencyTest\WrongCase\Plugin', $result['DependencyTest.WrongCase']);
$this->assertInstanceOf('DependencyTest\Dependency\Plugin', $result['DependencyTest.Dependency']);
}
public function testUnloadablePlugin()
{
$pluginNamespaces = $this->manager->getPluginNamespaces();
$result = $this->manager->loadPlugin('\\testvendor\\goto', $pluginNamespaces['\\testvendor\\goto']);
$this->assertNull($result);
}
public function testGetPluginPath()
{
$result = $this->manager->getPluginPath('Winter\Tester');
$basePath = str_replace('\\', '/', base_path());
$this->assertEquals($basePath . '/modules/system/tests/fixtures/plugins/winter/tester', $result);
}
public function testGetPlugins()
{
$result = $this->manager->getPlugins();
$this->assertCount(static::ENABLED_PLUGIN_COUNT, $result);
$this->assertArrayHasKey('Winter.NoUpdates', $result);
$this->assertArrayHasKey('Winter.Sample', $result);
$this->assertArrayHasKey('Winter.Tester', $result);
$this->assertArrayHasKey('Database.Tester', $result);
$this->assertArrayHasKey('TestVendor.Test', $result);
$this->assertArrayHasKey('DependencyTest.Found', $result);
$this->assertArrayHasKey('DependencyTest.WrongCase', $result);
$this->assertArrayHasKey('DependencyTest.Dependency', $result);
$this->assertArrayNotHasKey('DependencyTest.NotFound', $result);
$this->assertArrayNotHasKey('TestVendor.Goto', $result);
$this->assertInstanceOf('Winter\NoUpdates\Plugin', $result['Winter.NoUpdates']);
$this->assertInstanceOf('Winter\Sample\Plugin', $result['Winter.Sample']);
$this->assertInstanceOf('Winter\Tester\Plugin', $result['Winter.Tester']);
$this->assertInstanceOf('Database\Tester\Plugin', $result['Database.Tester']);
$this->assertInstanceOf('TestVendor\Test\Plugin', $result['TestVendor.Test']);
$this->assertInstanceOf('DependencyTest\Found\Plugin', $result['DependencyTest.Found']);
$this->assertInstanceOf('DependencyTest\WrongCase\Plugin', $result['DependencyTest.WrongCase']);
$this->assertInstanceOf('DependencyTest\Dependency\Plugin', $result['DependencyTest.Dependency']);
}
public function testFindByNamespace()
{
$result = $this->manager->findByNamespace('Winter\Tester');
$this->assertInstanceOf('Winter\Tester\Plugin', $result);
}
public function testHasPlugin()
{
$result = $this->manager->hasPlugin('Winter\Tester');
$this->assertTrue($result);
$result = $this->manager->hasPlugin('DependencyTest.Found');
$this->assertTrue($result);
$result = $this->manager->hasPlugin('DependencyTest\WrongCase');
$this->assertTrue($result);
$result = $this->manager->hasPlugin('DependencyTest\NotFound');
$this->assertTrue($result);
$result = $this->manager->hasPlugin('Winter\XXXXX');
$this->assertFalse($result);
/**
* Test case for https://github.com/octobercms/october/pull/4337
*/
$result = $this->manager->hasPlugin('dependencyTest\Wrongcase');
$this->assertTrue($result);
$result = $this->manager->hasPlugin('dependencyTest.Wrongcase');
$this->assertTrue($result);
}
public function testGetPluginNamespaces()
{
$result = $this->manager->getPluginNamespaces();
$this->assertCount(static::PLUGIN_NAMESPACE_COUNT, $result);
$this->assertArrayHasKey('\winter\noupdates', $result);
$this->assertArrayHasKey('\winter\sample', $result);
$this->assertArrayHasKey('\winter\tester', $result);
$this->assertArrayHasKey('\database\tester', $result);
$this->assertArrayHasKey('\testvendor\test', $result);
$this->assertArrayHasKey('\testvendor\goto', $result);
$this->assertArrayHasKey('\dependencytest\found', $result);
$this->assertArrayHasKey('\dependencytest\notfound', $result);
$this->assertArrayHasKey('\dependencytest\wrongcase', $result);
$this->assertArrayHasKey('\dependencytest\dependency', $result);
}
public function testGetVendorAndPluginNames()
{
$vendors = $this->manager->getVendorAndPluginNames();
$this->assertCount(static::PLUGIN_VENDOR_COUNT, $vendors);
$this->assertArrayHasKey('winter', $vendors);
$this->assertArrayHasKey('noupdates', $vendors['winter']);
$this->assertArrayHasKey('sample', $vendors['winter']);
$this->assertArrayHasKey('tester', $vendors['winter']);
$this->assertArrayHasKey('database', $vendors);
$this->assertArrayHasKey('tester', $vendors['database']);
$this->assertArrayHasKey('testvendor', $vendors);
$this->assertArrayHasKey('test', $vendors['testvendor']);
$this->assertArrayHasKey('goto', $vendors['testvendor']);
$this->assertArrayHasKey('dependencytest', $vendors);
$this->assertArrayHasKey('found', $vendors['dependencytest']);
$this->assertArrayHasKey('notfound', $vendors['dependencytest']);
$this->assertArrayHasKey('wrongcase', $vendors['dependencytest']);
$this->assertArrayHasKey('dependency', $vendors['dependencytest']);
}
public function testPluginDetails()
{
$testPlugin = $this->manager->findByNamespace('Winter\XXXXX');
$this->assertNull($testPlugin);
$testPlugin = $this->manager->findByNamespace('Winter\Tester');
$this->assertNotNull($testPlugin);
$pluginDetails = $testPlugin->pluginDetails();
$this->assertEquals('Winter Test Plugin', $pluginDetails['name']);
$this->assertEquals('Test plugin used by unit tests.', $pluginDetails['description']);
$this->assertEquals('Alexey Bobkov, Samuel Georges', $pluginDetails['author']);
}
public function testUnregisterall()
{
$result = $this->manager->getPlugins();
$this->assertCount(static::ENABLED_PLUGIN_COUNT, $result);
$this->manager->unregisterAll();
$this->assertEmpty($this->manager->getPlugins());
}
public function testGetDependencies()
{
$result = $this->manager->getDependencies('DependencyTest.Found');
$this->assertCount(1, $result);
$this->assertContains('DependencyTest.Dependency', $result);
$result = $this->manager->getDependencies('DependencyTest.WrongCase');
$this->assertCount(1, $result);
$this->assertContains('Dependencytest.dependency', $result);
$result = $this->manager->getDependencies('DependencyTest.NotFound');
$this->assertCount(1, $result);
$this->assertContains('DependencyTest.Missing', $result);
}
public function testIsDisabled()
{
$result = $this->manager->isDisabled('DependencyTest.Found');
$this->assertFalse($result);
$result = $this->manager->isDisabled('DependencyTest.WrongCase');
$this->assertFalse($result);
$result = $this->manager->isDisabled('DependencyTest.NotFound');
$this->assertTrue($result);
/**
* Test case for https://github.com/octobercms/october/pull/4838
*/
$result = $this->manager->isDisabled('dependencyTest\Wrongcase');
$this->assertFalse($result);
$result = $this->manager->isDisabled('dependencyTest.Wrongcase');
$this->assertFalse($result);
$result = $this->manager->isDisabled('dependencytest.notfound');
$this->assertTrue($result);
}
public function testExists()
{
$result = $this->manager->exists('DependencyTest.Found');
$this->assertTrue($result);
$result = $this->manager->exists('DependencyTest.WrongCase');
$this->assertTrue($result);
$result = $this->manager->exists('DependencyTest.NotFound');
$this->assertFalse($result);
$result = $this->manager->exists('Unknown.Plugin');
$this->assertFalse($result);
}
public function testReplacement()
{
$this->assertFalse($this->manager->isDisabled('Winter.Replacement'));
$this->assertTrue($this->manager->isDisabled('Winter.Original'));
$this->assertTrue($this->manager->isDisabled('Winter.InvalidReplacement'));
$this->assertEquals('Winter.Replacement', $this->manager->findByIdentifier('Winter.Original')->getPluginIdentifier());
}
public function testHasPluginReplacement()
{
// check a replaced plugin
$this->assertTrue($this->manager->hasPlugin('Winter.Original'));
$this->assertTrue($this->manager->isDisabled('Winter.Original'));
// check a replacement plugin
$this->assertTrue($this->manager->hasPlugin('Winter.Replacement'));
$this->assertFalse($this->manager->isDisabled('Winter.Replacement'));
// check a plugin where the replacement is invalid
$this->assertTrue($this->manager->hasPlugin('Winter.InvalidReplacement'));
$this->assertTrue($this->manager->isDisabled('Winter.InvalidReplacement'));
// check a plugin replacing a plugin not found on disk
$this->assertTrue($this->manager->hasPlugin('Winter.ReplaceNotInstalled'));
$this->assertFalse($this->manager->isDisabled('Winter.ReplaceNotInstalled'));
// ensure searching for the alias of a replacement (plugin not installed)
$this->assertTrue($this->manager->hasPlugin('Winter.NotInstalled'));
$this->assertInstanceOf(\Winter\Replacement\Plugin::class, $this->manager->findByIdentifier('Winter.Original'));
$this->assertInstanceOf(\Winter\Replacement\Plugin::class, $this->manager->findByIdentifier('Winter.Replacement'));
// check getting a plugin via it's not installed original plugin identifier
$this->assertInstanceOf(\Winter\ReplaceNotInstalled\Plugin::class, $this->manager->findByIdentifier('Winter.NotInstalled'));
$this->assertNull($this->manager->findByIdentifier('Winter.NotInstalled', true));
// force getting the original plugin
$this->assertInstanceOf(\Winter\Original\Plugin::class, $this->manager->findByIdentifier('Winter.Original', true));
}
public function testHasPluginReplacementMixedCase()
{
// test checking casing of installed plugin (resolved via getNormalizedIdentifier())
$this->assertTrue($this->manager->hasPlugin('Winter.ReplaceNotInstalled'));
$this->assertTrue($this->manager->hasPlugin('Winter.replaceNotInstalled'));
$this->assertTrue($this->manager->hasPlugin('Winter.replacenotInstalled'));
$this->assertTrue($this->manager->hasPlugin('winter.replacenotInstalled'));
$this->assertTrue($this->manager->hasPlugin('winter.replacenotinstalled'));
// test checking casing of installed replaced plugin (resolved via getNormalizedIdentifier() & replacementMap)
$this->assertTrue($this->manager->hasPlugin('Winter.Original'));
$this->assertTrue($this->manager->hasPlugin('Winter.original'));
$this->assertTrue($this->manager->hasPlugin('winter.original'));
// test checking casing of uninstalled plugin (resolved via strtolower() on replacement keys)
$this->assertTrue($this->manager->hasPlugin('Winter.NotInstalled'));
$this->assertTrue($this->manager->hasPlugin('Winter.notInstalled'));
$this->assertTrue($this->manager->hasPlugin('winter.notInstalled'));
$this->assertTrue($this->manager->hasPlugin('Winter.notinstalled'));
}
public function testExistsReplacementMixedCase()
{
// test checking casing of installed plugin (resolved via getNormalizedIdentifier())
$this->assertTrue($this->manager->exists('Winter.ReplaceNotInstalled'));
$this->assertTrue($this->manager->exists('Winter.replaceNotInstalled'));
$this->assertTrue($this->manager->exists('Winter.replacenotInstalled'));
$this->assertTrue($this->manager->exists('winter.replacenotInstalled'));
$this->assertTrue($this->manager->exists('winter.replacenotinstalled'));
// test checking casing of installed replaced plugin (resolved via getNormalizedIdentifier() & replacementMap)
$this->assertFalse($this->manager->exists('Winter.Original'));
$this->assertFalse($this->manager->exists('Winter.original'));
$this->assertFalse($this->manager->exists('winter.original'));
// test checking casing of uninstalled plugin (resolved via strtolower() on replacement keys)
$this->assertTrue($this->manager->exists('Winter.NotInstalled'));
$this->assertTrue($this->manager->exists('Winter.notInstalled'));
$this->assertTrue($this->manager->exists('winter.notInstalled'));
$this->assertTrue($this->manager->exists('Winter.notinstalled'));
}
public function testFindByIdentifierReplacementMixedCase()
{
// test resolving plugin with mixed casing
$this->assertInstanceOf(\Winter\ReplaceNotInstalled\Plugin::class, $this->manager->findByIdentifier('Winter.ReplaceNotInstalled'));
$this->assertInstanceOf(\Winter\ReplaceNotInstalled\Plugin::class, $this->manager->findByIdentifier('Winter.replaceNotInstalled'));
$this->assertInstanceOf(\Winter\ReplaceNotInstalled\Plugin::class, $this->manager->findByIdentifier('Winter.replacenotInstalled'));
$this->assertInstanceOf(\Winter\ReplaceNotInstalled\Plugin::class, $this->manager->findByIdentifier('winter.replacenotInstalled'));
$this->assertInstanceOf(\Winter\ReplaceNotInstalled\Plugin::class, $this->manager->findByIdentifier('winter.replacenotinstalled'));
// test resolving replacement plugin with mixed casing
$this->assertInstanceOf(\Winter\Replacement\Plugin::class, $this->manager->findByIdentifier('Winter.Original'));
$this->assertInstanceOf(\Winter\Replacement\Plugin::class, $this->manager->findByIdentifier('Winter.original'));
$this->assertInstanceOf(\Winter\Replacement\Plugin::class, $this->manager->findByIdentifier('winter.original'));
// test resolving original plugin with mixed casing when ignoring replacements
$this->assertInstanceOf(\Winter\Original\Plugin::class, $this->manager->findByIdentifier('Winter.Original', true));
$this->assertInstanceOf(\Winter\Original\Plugin::class, $this->manager->findByIdentifier('Winter.original', true));
$this->assertInstanceOf(\Winter\Original\Plugin::class, $this->manager->findByIdentifier('winter.original', true));
// test resolving replacement plugin of uninstalled plugin with mixed casing
$this->assertInstanceOf(\Winter\ReplaceNotInstalled\Plugin::class, $this->manager->findByIdentifier('Winter.NotInstalled'));
$this->assertInstanceOf(\Winter\ReplaceNotInstalled\Plugin::class, $this->manager->findByIdentifier('Winter.notInstalled'));
$this->assertInstanceOf(\Winter\ReplaceNotInstalled\Plugin::class, $this->manager->findByIdentifier('winter.notInstalled'));
$this->assertInstanceOf(\Winter\ReplaceNotInstalled\Plugin::class, $this->manager->findByIdentifier('Winter.notinstalled'));
}
public function testGetReplacements()
{
$replacementPluginReplaces = $this->manager->findByIdentifier('Winter.Replacement')->getReplaces();
$this->assertIsArray($replacementPluginReplaces);
$this->assertCount(1, $replacementPluginReplaces);
$this->assertEquals('Winter.Original', $replacementPluginReplaces[0]);
$invalidPluginReplaces = $this->manager->findByIdentifier('Winter.InvalidReplacement')->getReplaces();
$this->assertIsArray($invalidPluginReplaces);
$this->assertCount(1, $invalidPluginReplaces);
$this->assertEquals('Winter.Tester', $invalidPluginReplaces[0]);
}
public function testReplaceVersion()
{
$invalidReplacementPlugin = $this->manager->findByIdentifier('Winter.InvalidReplacement');
$this->assertTrue($invalidReplacementPlugin->canReplacePlugin('Winter.Tester', '9'));
$this->assertTrue($invalidReplacementPlugin->canReplacePlugin('Winter.Tester', '9.0'));
$this->assertTrue($invalidReplacementPlugin->canReplacePlugin('Winter.Tester', '11.0.0'));
$this->assertFalse($invalidReplacementPlugin->canReplacePlugin('Winter.Tester', '8.0'));
$replacementPlugin = $this->manager->findByIdentifier('Winter.Replacement');
$this->assertTrue($replacementPlugin->canReplacePlugin('Winter.Original', '1.0.2'));
$this->assertTrue($replacementPlugin->canReplacePlugin('Winter.Original', '1.0'));
$this->assertFalse($replacementPlugin->canReplacePlugin('Winter.Original', '2.0.1'));
}
public function testActiveReplacementMap()
{
$map = $this->manager->getActiveReplacementMap();
$this->assertArrayHasKey('Winter.Original', $map);
$this->assertEquals('Winter.Replacement', $map['Winter.Original']);
$this->assertEquals('Winter.Replacement', $this->manager->getActiveReplacementMap('Winter.Original'));
$this->assertNull($this->manager->getActiveReplacementMap('Winter.InvalidReplacement'));
}
public function testFlagDisableStatus()
{
$plugin = $this->manager->findByIdentifier('DependencyTest.Dependency');
$flags = $this->manager->getPluginFlags($plugin);
$this->assertEmpty($flags);
$plugin = $this->manager->findByIdentifier('DependencyTest.NotFound');
$flags = $this->manager->getPluginFlags($plugin);
$this->assertCount(1, $flags);
$this->assertArrayHasKey(PluginManager::DISABLED_MISSING_DEPENDENCIES, $flags);
$plugin = $this->manager->findByIdentifier('Winter.InvalidReplacement');
$flags = $this->manager->getPluginFlags($plugin);
$this->assertCount(1, $flags);
$this->assertArrayHasKey(PluginManager::DISABLED_REPLACEMENT_FAILED, $flags);
$plugin = $this->manager->findByIdentifier('Winter.Original', true);
$flags = $this->manager->getPluginFlags($plugin);
$this->assertCount(1, $flags);
$this->assertArrayHasKey(PluginManager::DISABLED_REPLACED, $flags);
}
public function testFlagDisabling()
{
$plugin = $this->manager->findByIdentifier('Winter.Tester', true);
$flags = $this->manager->getPluginFlags($plugin);
$this->assertEmpty($flags);
$this->manager->disablePlugin($plugin);
$flags = $this->manager->getPluginFlags($plugin);
$this->assertCount(1, $flags);
$this->assertArrayHasKey(PluginManager::DISABLED_BY_USER, $flags);
$this->manager->enablePlugin($plugin);
$flags = $this->manager->getPluginFlags($plugin);
$this->assertEmpty($flags);
$this->manager->disablePlugin($plugin, PluginManager::DISABLED_BY_CONFIG);
$flags = $this->manager->getPluginFlags($plugin);
$this->assertCount(1, $flags);
$this->assertArrayHasKey(PluginManager::DISABLED_BY_CONFIG, $flags);
$this->manager->enablePlugin($plugin, PluginManager::DISABLED_BY_CONFIG);
$flags = $this->manager->getPluginFlags($plugin);
$this->assertEmpty($flags);
}
public function testPluginNormalization()
{
// test lower to upper
$this->assertEquals('Database.Tester', $this->manager->normalizeIdentifier('database.tester'));
$this->assertEquals('Database.Tester', $this->manager->getNormalizedIdentifier('database.tester'));
// test exact match
$this->assertEquals('DependencyTest.Found', $this->manager->normalizeIdentifier('DependencyTest.Found'));
$this->assertEquals('DependencyTest.Found', $this->manager->getNormalizedIdentifier('DependencyTest.Found'));
// test mixed case
$this->assertEquals('DependencyTest.Found', $this->manager->normalizeIdentifier('Dependencytest.Found'));
$this->assertEquals('DependencyTest.Found', $this->manager->getNormalizedIdentifier('Dependencytest.Found'));
// test typeo
$this->assertEquals('dpendencytest.Found', $this->manager->normalizeIdentifier('dpendencytest.Found'));
$this->assertEquals('dpendencytest.Found', $this->manager->getNormalizedIdentifier('dpendencytest.Found'));
$this->assertEquals('Winter.NoUpdate', $this->manager->normalizeIdentifier('Winter.NoUpdate'));
$this->assertEquals('Winter.NoUpdate', $this->manager->getNormalizedIdentifier('Winter.NoUpdate'));
// test multiple mixed case installed plugin
$this->assertEquals('Winter.NoUpdates', $this->manager->normalizeIdentifier('Winter.NoUpdates'));
$this->assertEquals('Winter.NoUpdates', $this->manager->normalizeIdentifier('winter.noUpdates'));
$this->assertEquals('Winter.NoUpdates', $this->manager->normalizeIdentifier('winter.noupdates'));
$this->assertEquals('Winter.NoUpdates', $this->manager->getNormalizedIdentifier('Winter.NoUpdates'));
$this->assertEquals('Winter.NoUpdates', $this->manager->getNormalizedIdentifier('winter.noUpdates'));
$this->assertEquals('Winter.NoUpdates', $this->manager->getNormalizedIdentifier('winter.noupdates'));
// test multiple mixed case not installed plugin
$this->assertEquals('Winter.MissingPlugin', $this->manager->normalizeIdentifier('Winter.MissingPlugin'));
$this->assertEquals('Winter.Missingplugin', $this->manager->normalizeIdentifier('Winter.Missingplugin'));
$this->assertEquals('Winter.missingplugin', $this->manager->normalizeIdentifier('Winter.missingplugin'));
$this->assertEquals('winter.missingplugin', $this->manager->normalizeIdentifier('winter.missingplugin'));
$this->assertEquals('Winter.MissingPlugin', $this->manager->getNormalizedIdentifier('Winter.MissingPlugin'));
$this->assertEquals('Winter.Missingplugin', $this->manager->getNormalizedIdentifier('Winter.Missingplugin'));
$this->assertEquals('Winter.missingplugin', $this->manager->getNormalizedIdentifier('Winter.missingplugin'));
$this->assertEquals('winter.missingplugin', $this->manager->getNormalizedIdentifier('winter.missingplugin'));
// test passing plugin object
$plugin = $this->manager->findByIdentifier('Winter.NoUpdates');
$this->assertInstanceOf(PluginBase::class, $plugin);
$this->assertEquals('Winter.NoUpdates', $this->manager->getNormalizedIdentifier($plugin));
}
public function testSortPluginDependencies()
{
$result = $this->manager->getPlugins();
$this->assertGreaterThan(
array_search('DependencyTest.Dependency', array_keys($result)),
array_search('DependencyTest.Found', array_keys($result))
);
// check to make sure dependency comes first and didn't stay in alphanumeric order.
$this->assertGreaterThan(
array_search('DependencyTest.Dependency', array_keys($result)),
array_search('DependencyTest.Acme', array_keys($result))
);
}
}

View File

@@ -0,0 +1,341 @@
<?php
namespace System\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use Winter\Storm\Argon\Argon;
use System\Classes\SourceManifest;
use System\Classes\FileManifest;
class SourceManifestTest extends TestCase
{
/** @var SourceManifest instance */
protected $sourceManifest;
/** @var array Emulated builds from the manifest fixture */
protected $builds;
public function setUp(): void
{
parent::setUp();
$this->builds = [
'1.0.0' => new FileManifest(
base_path('modules/system/tests/fixtures/manifest/1_0_0'),
['test', 'test2']
),
'1.0.1' => new FileManifest(
base_path('modules/system/tests/fixtures/manifest/1_0_1'),
['test', 'test2']
),
'1.0.2' => new FileManifest(
base_path('modules/system/tests/fixtures/manifest/1_0_2'),
['test', 'test2']
),
'1.1.0' => new FileManifest(
base_path('modules/system/tests/fixtures/manifest/1_1_0'),
['test', 'test2', 'test3']
),
'1.1.1' => new FileManifest(
base_path('modules/system/tests/fixtures/manifest/1_1_1'),
['test', 'test2', 'test3']
),
'1.0.3' => new FileManifest(
base_path('modules/system/tests/fixtures/manifest/1_0_3'),
['test', 'test2']
),
];
$this->sourceManifest = new SourceManifest($this->manifestPath(), $this->forksPath(), false);
}
public function tearDown(): void
{
Argon::setTestNow();
$this->deleteManifest();
}
public function testCreateManifest()
{
// Freeze date for test
$testDate = Argon::create(2020, 12, 16, 12, 01, 0, 'UTC');
Argon::setTestNow($testDate);
$this->createManifest(true);
$this->assertEquals(
'{' . "\n" .
' "_description": "This is the source manifest of changes to Winter CMS for each version. This is used to determine which version of Winter CMS is in use, via the \"winter:version\" Artisan command.",' . "\n" .
' "_created": "2020-12-16T12:01:00+00:00",' . "\n" .
' "manifest": [' . "\n" .
' {' . "\n" .
' "build": "1.0.0",' . "\n" .
' "parent": null,' . "\n" .
' "modules": {' . "\n" .
' "test": "e1d6c6e4c482688e231ee37d89668268426512013695de47bfcb424f9a645c7b",' . "\n" .
' "test2": "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"' . "\n" .
' },' . "\n" .
' "files": {' . "\n" .
' "added": {' . "\n" .
' "\/modules\/test\/file1.php": "6f9b0b94528a85b2a6bb67b5621e074aef1b4c9fc9ee3ea1bd69100ea14cb3db"' . "\n" .
' }' . "\n" .
' }' . "\n" .
' },' . "\n" .
' {' . "\n" .
' "build": "1.0.1",' . "\n" .
' "parent": "1.0.0",' . "\n" .
' "modules": {' . "\n" .
' "test": "c0b794ff210862a4ce16223802efe6e28969f5a4fb42480ec8c2fef2da23d181",' . "\n" .
' "test2": "32c9f2fb6e0a22dde288a0fe1e4834798360b25e5a91d2597409d9302221381d"' . "\n" .
' },' . "\n" .
' "files": {' . "\n" .
' "added": {' . "\n" .
' "\/modules\/test\/file2.php": "96ae9f6b6377ad29226ea169f952de49fc29ae895f18a2caed76aeabdf050f1b",' . "\n" .
' "\/modules\/test2\/file1.php": "94bd47b1ac7b2837b31883ebcd38c8101687321f497c3c4b9744f68ae846721d"' . "\n" .
' }' . "\n" .
' }' . "\n" .
' },' . "\n" .
' {' . "\n" .
' "build": "1.0.2",' . "\n" .
' "parent": "1.0.1",' . "\n" .
' "modules": {' . "\n" .
' "test": "419a3c073a4296213cdc9319cfc488383753e2e81cefa1c73db38749b82a3c51",' . "\n" .
' "test2": "32c9f2fb6e0a22dde288a0fe1e4834798360b25e5a91d2597409d9302221381d"' . "\n" .
' },' . "\n" .
' "files": {' . "\n" .
' "added": {' . "\n" .
' "\/modules\/test\/file3.php": "7f4132b05911a6b0df4d41bf5dc3d007786b63a5a22daf3060ed222816d57b54"' . "\n" .
' },' . "\n" .
' "modified": {' . "\n" .
' "\/modules\/test\/file2.php": "2c61b2f5688275574251a19a57e06a4eb9e537b3916ebf6f71768e184a4ae538"' . "\n" .
' },' . "\n" .
' "removed": [' . "\n" .
' "\/modules\/test\/file1.php"' . "\n" .
' ]' . "\n" .
' }' . "\n" .
' },' . "\n" .
' {' . "\n" .
' "build": "1.0.3",' . "\n" .
' "parent": "1.0.2",' . "\n" .
' "modules": {' . "\n" .
' "test": "5316f172ac24aaa7713c97885e2e27f5c5c1e02f96fcb8e269903d737d52c7bd",' . "\n" .
' "test2": "18fc53cb280cc7e43d47e55a66b1245992c331f368106c13024572dc9fb215f7"' . "\n" .
' },' . "\n" .
' "files": {' . "\n" .
' "modified": {' . "\n" .
' "\/modules\/test2\/file1.php": "e284e816365653b5f7ddfca9319c24716cadfa3538e0c79994f0416e964da513"' . "\n" .
' },' . "\n" .
' "removed": [' . "\n" .
' "\/modules\/test\/file3.php"' . "\n" .
' ]' . "\n" .
' }' . "\n" .
' },' . "\n" .
' {' . "\n" .
' "build": "1.1.0",' . "\n" .
' "parent": "1.0.2",' . "\n" .
' "modules": {' . "\n" .
' "test": "e30811c9ad3119394edefd2f2fc0bae5fc08e2a03220ccc94e2a3f078da0df6d",' . "\n" .
' "test2": "2066b3cffe4f03b06c3399dba4d56147e4247b8071e19fa597a2e15dec697f5e",' . "\n" .
' "test3": "3679fa86c8d92a4474a01fc41abc24ebf34b847d7288434028a0559a10ff5d33"' . "\n" .
' },' . "\n" .
' "files": {' . "\n" .
' "added": {' . "\n" .
' "\/modules\/test\/file4.php": "18047babf2625ed8f42d779f14539449e23ccfdc92f79818f291eb1c55ff0533",' . "\n" .
' "\/modules\/test3\/file1.php": "1b691b48b8247af3dd8046a70e5678b77e66ada13b86c3a269a6428425c3a835"' . "\n" .
' },' . "\n" .
' "modified": {' . "\n" .
' "\/modules\/test\/file3.php": "65b1d6f8a1a0f2dfbe870faba9fa2ee0b6f4c291656df6cd66158e166a2680eb",' . "\n" .
' "\/modules\/test2\/file1.php": "5197bcc06443799c71fea8cb608cee61d9edd611ea23ea603aa0420393e014ca"' . "\n" .
' },' . "\n" .
' "removed": [' . "\n" .
' "\/modules\/test\/file2.php"' . "\n" .
' ]' . "\n" .
' }' . "\n" .
' },' . "\n" .
' {' . "\n" .
' "build": "1.1.1",' . "\n" .
' "parent": "1.1.0",' . "\n" .
' "modules": {' . "\n" .
' "test": "e30811c9ad3119394edefd2f2fc0bae5fc08e2a03220ccc94e2a3f078da0df6d",' . "\n" .
' "test2": "18fc53cb280cc7e43d47e55a66b1245992c331f368106c13024572dc9fb215f7",' . "\n" .
' "test3": "728ff099502e8d40cc6a432fdeba7185b9cb5cb530dcd8f5741ec56fcd37d189"' . "\n" .
' },' . "\n" .
' "files": {' . "\n" .
' "added": {' . "\n" .
' "\/modules\/test3\/file2.php": "0614afe438a4e74547a7036ef895008c36895a783eb8e0b458691f631138d310"' . "\n" .
' },' . "\n" .
' "modified": {' . "\n" .
' "\/modules\/test2\/file1.php": "e284e816365653b5f7ddfca9319c24716cadfa3538e0c79994f0416e964da513",' . "\n" .
' "\/modules\/test3\/file1.php": "d9aeb0853bc0f21a836f2d1cd5fc224d64ce8ea9e7e22c43b1a00f25d37c42f8"' . "\n" .
' }' . "\n" .
' }' . "\n" .
' }' . "\n" .
' ]' . "\n" .
'}',
file_get_contents($this->manifestPath())
);
}
public function testGetBuilds()
{
$this->createManifest();
$buildKeys = $this->sourceManifest->getBuilds();
$this->assertCount(6, $buildKeys);
$this->assertEquals(['1.0.0', '1.0.1', '1.0.2', '1.0.3', '1.1.0', '1.1.1'], $buildKeys);
}
public function testGetState()
{
$this->createManifest();
$this->assertEquals([
'/modules/test/file1.php' => '6f9b0b94528a85b2a6bb67b5621e074aef1b4c9fc9ee3ea1bd69100ea14cb3db',
], $this->sourceManifest->getState('1.0.0'));
$this->assertEquals([
'/modules/test/file1.php' => '6f9b0b94528a85b2a6bb67b5621e074aef1b4c9fc9ee3ea1bd69100ea14cb3db',
'/modules/test/file2.php' => '96ae9f6b6377ad29226ea169f952de49fc29ae895f18a2caed76aeabdf050f1b',
'/modules/test2/file1.php' => '94bd47b1ac7b2837b31883ebcd38c8101687321f497c3c4b9744f68ae846721d',
], $this->sourceManifest->getState('1.0.1'));
$this->assertEquals([
'/modules/test/file2.php' => '2c61b2f5688275574251a19a57e06a4eb9e537b3916ebf6f71768e184a4ae538',
'/modules/test/file3.php' => '7f4132b05911a6b0df4d41bf5dc3d007786b63a5a22daf3060ed222816d57b54',
'/modules/test2/file1.php' => '94bd47b1ac7b2837b31883ebcd38c8101687321f497c3c4b9744f68ae846721d',
], $this->sourceManifest->getState('1.0.2'));
$this->assertEquals([
'/modules/test/file2.php' => '2c61b2f5688275574251a19a57e06a4eb9e537b3916ebf6f71768e184a4ae538',
'/modules/test2/file1.php' => 'e284e816365653b5f7ddfca9319c24716cadfa3538e0c79994f0416e964da513',
], $this->sourceManifest->getState('1.0.3'));
$this->assertEquals([
'/modules/test/file3.php' => '65b1d6f8a1a0f2dfbe870faba9fa2ee0b6f4c291656df6cd66158e166a2680eb',
'/modules/test/file4.php' => '18047babf2625ed8f42d779f14539449e23ccfdc92f79818f291eb1c55ff0533',
'/modules/test2/file1.php' => '5197bcc06443799c71fea8cb608cee61d9edd611ea23ea603aa0420393e014ca',
'/modules/test3/file1.php' => '1b691b48b8247af3dd8046a70e5678b77e66ada13b86c3a269a6428425c3a835',
], $this->sourceManifest->getState('1.1.0'));
$this->assertEquals([
'/modules/test/file3.php' => '65b1d6f8a1a0f2dfbe870faba9fa2ee0b6f4c291656df6cd66158e166a2680eb',
'/modules/test/file4.php' => '18047babf2625ed8f42d779f14539449e23ccfdc92f79818f291eb1c55ff0533',
'/modules/test2/file1.php' => 'e284e816365653b5f7ddfca9319c24716cadfa3538e0c79994f0416e964da513',
'/modules/test3/file1.php' => 'd9aeb0853bc0f21a836f2d1cd5fc224d64ce8ea9e7e22c43b1a00f25d37c42f8',
'/modules/test3/file2.php' => '0614afe438a4e74547a7036ef895008c36895a783eb8e0b458691f631138d310',
], $this->sourceManifest->getState('1.1.1'));
}
public function testCompare()
{
$this->createManifest();
$this->assertEquals([
'build' => '1.0.0',
'modified' => false,
'confident' => true
], $this->sourceManifest->compare($this->builds['1.0.0']));
$this->assertEquals([
'build' => '1.0.1',
'modified' => false,
'confident' => true
], $this->sourceManifest->compare($this->builds['1.0.1']));
$this->assertEquals([
'build' => '1.0.2',
'modified' => false,
'confident' => true
], $this->sourceManifest->compare($this->builds['1.0.2']));
$this->assertEquals([
'build' => '1.0.3',
'modified' => false,
'confident' => true
], $this->sourceManifest->compare($this->builds['1.0.3']));
$this->assertEquals([
'build' => '1.1.0',
'modified' => false,
'confident' => true
], $this->sourceManifest->compare($this->builds['1.1.0']));
$this->assertEquals([
'build' => '1.1.1',
'modified' => false,
'confident' => true
], $this->sourceManifest->compare($this->builds['1.1.1']));
}
public function testCompareModified()
{
$this->createManifest();
// Hot-swap "tests/fixtures/manifest/1_1_1/modules/test/file3.php"
$old = file_get_contents(base_path('modules/system/tests/fixtures/manifest/1_1_1/modules/test/file3.php'));
file_put_contents(base_path('modules/system/tests/fixtures/manifest/1_1_1/modules/test/file3.php'), '<?php // Changed');
$modifiedManifest = new FileManifest(base_path('modules/system/tests/fixtures/manifest/1_1_1'), ['test', 'test2']);
$modifiedManifest->getFiles();
file_put_contents(base_path('modules/system/tests/fixtures/manifest/1_1_1/modules/test/file3.php'), $old);
$this->assertEquals([
'build' => '1.1.1',
'modified' => true,
'confident' => true,
], $this->sourceManifest->compare($modifiedManifest));
}
public function testCompareModifiedSecondBranch()
{
$this->createManifest();
// Add "tests/fixtures/manifest/1_0_3/modules/test/file3.php"
file_put_contents(base_path('modules/system/tests/fixtures/manifest/1_0_3/modules/test/file3.php'), '<?php // Changed');
$modifiedManifest = new FileManifest(base_path('modules/system/tests/fixtures/manifest/1_0_3'), ['test', 'test2']);
$modifiedManifest->getFiles();
unlink(base_path('modules/system/tests/fixtures/manifest/1_0_3/modules/test/file3.php'));
$this->assertEquals([
'build' => '1.0.3',
'modified' => true,
'confident' => false, // 50% match, not confident
], $this->sourceManifest->compare($modifiedManifest));
}
protected function createManifest(bool $write = false)
{
$this->deleteManifest();
foreach ($this->builds as $build => $fileManifest) {
$this->sourceManifest->addBuild($build, $fileManifest);
}
if ($write) {
file_put_contents($this->manifestPath(), $this->sourceManifest->generate());
}
$this->sourceManifest->loadForks();
}
protected function deleteManifest()
{
if (file_exists($this->manifestPath())) {
unlink($this->manifestPath());
}
}
protected function manifestPath()
{
return base_path('modules/system/tests/fixtures/manifest/builds.json');
}
protected function forksPath()
{
return base_path('modules/system/tests/fixtures/manifest/forks.json');
}
}

View File

@@ -0,0 +1,258 @@
<?php
namespace System\Tests\Classes;
use System\Tests\Bootstrap\TestCase;
use System\Classes\VersionManager;
class VersionManagerTest extends TestCase
{
public function setUp() : void
{
parent::setUp();
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/Plugin.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/sample/Plugin.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/noupdates/Plugin.php';
}
//
// Tests
//
public function testGetLatestFileVersion()
{
$manager = VersionManager::instance();
$result = self::callProtectedMethod($manager, 'getLatestFileVersion', ['\Winter\\Tester']);
$this->assertNotNull($result);
$this->assertEquals('1.5.1', $result);
}
public function testGetFileVersions()
{
$manager = VersionManager::instance();
$result = self::callProtectedMethod($manager, 'getFileVersions', ['\Winter\\Tester']);
$this->assertCount(13, $result);
$this->assertArrayHasKey('1.0.1', $result);
$this->assertArrayHasKey('1.0.2', $result);
$this->assertArrayHasKey('1.0.3', $result);
$this->assertArrayHasKey('1.0.4', $result);
$this->assertArrayHasKey('1.0.5', $result);
$this->assertArrayHasKey('1.1.0', $result);
$this->assertArrayHasKey('1.2.0', $result);
$this->assertArrayHasKey('1.3.0', $result);
$this->assertArrayHasKey('1.3.1', $result);
$this->assertArrayHasKey('1.3.2', $result);
$this->assertArrayHasKey('1.4.1', $result);
$this->assertArrayHasKey('1.5.0', $result);
$this->assertArrayHasKey('1.5.1', $result);
$sample = $result['1.0.1'];
$this->assertEquals('Added some upgrade file and some "seeding"', $sample[0]);
$sample = $result['1.1.0'];
$this->assertEquals('!!! Drop support for blog settings', $sample[0]);
$this->assertEquals('drop_blog_settings_table.php', $sample[1]);
$sample = $result['1.2.0'];
$this->assertEquals('!!! Security update - see: https://wintercms.com', $sample[0]);
$sample = $result['1.3.0'];
$this->assertEquals('!!! We\'ve refactored major parts of this plugin. Please see the website for more information.', $sample);
$sample = $result['1.3.1'];
$this->assertEquals('Minor bug fix Please see changelog', $sample[0]);
$this->assertEquals('fix_database.php', $sample[1]);
$sample = $result['1.3.2'];
$this->assertEquals('Added support for Translate plugin. Added some new languages.', $sample);
$sample = $result['1.5.0'];
$this->assertEquals('!!! Another major update to fix several issues', $sample);
$sample = $result['1.5.1'];
$this->assertEquals('Improved signature with the Test::method()', $sample[0]);
$this->assertEquals('Translation updates.', $sample[1]);
/*
* Test junk file
*/
$result = self::callProtectedMethod($manager, 'getFileVersions', ['\Winter\\Sample']);
$this->assertCount(5, $result);
$this->assertArrayHasKey('junk', $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('1.0.*', $result);
$this->assertArrayHasKey('1.0.x', $result);
$this->assertArrayHasKey('10.3', $result);
$sample = array_shift($result);
$comment = array_shift($sample);
$this->assertEquals("JUNK JUNK JUNK", $comment);
/*
* Test empty file
*/
$result = self::callProtectedMethod($manager, 'getFileVersions', ['\Winter\\NoUpdates']);
$this->assertEmpty($result);
}
public function testGetNewFileVersions()
{
$manager = VersionManager::instance();
$result = self::callProtectedMethod($manager, 'getNewFileVersions', ['\Winter\\Tester', '1.0.3']);
$this->assertCount(10, $result);
$this->assertArrayHasKey('1.0.4', $result);
$this->assertArrayHasKey('1.0.5', $result);
$this->assertArrayHasKey('1.1.0', $result);
$this->assertArrayHasKey('1.2.0', $result);
$this->assertArrayHasKey('1.3.0', $result);
$this->assertArrayHasKey('1.3.1', $result);
$this->assertArrayHasKey('1.3.2', $result);
$this->assertArrayHasKey('1.4.1', $result);
$this->assertArrayHasKey('1.5.0', $result);
$this->assertArrayHasKey('1.5.1', $result);
/*
* When at version 0, should return everything
*/
$manager = VersionManager::instance();
$result = self::callProtectedMethod($manager, 'getNewFileVersions', ['\Winter\\Tester']);
$this->assertCount(13, $result);
$this->assertArrayHasKey('1.0.1', $result);
$this->assertArrayHasKey('1.0.2', $result);
$this->assertArrayHasKey('1.0.3', $result);
$this->assertArrayHasKey('1.0.4', $result);
$this->assertArrayHasKey('1.0.5', $result);
$this->assertArrayHasKey('1.1.0', $result);
$this->assertArrayHasKey('1.2.0', $result);
$this->assertArrayHasKey('1.3.0', $result);
$this->assertArrayHasKey('1.3.1', $result);
$this->assertArrayHasKey('1.3.2', $result);
$this->assertArrayHasKey('1.4.1', $result);
$this->assertArrayHasKey('1.5.0', $result);
$this->assertArrayHasKey('1.5.1', $result);
}
/**
* @dataProvider versionInfoProvider
*
* @param $versionInfo
* @param $expectedComments
* @param $expectedScripts
*/
public function testExtractScriptsAndComments($versionInfo, $expectedComments, $expectedScripts)
{
$manager = VersionManager::instance();
list($comments, $scripts) = self::callProtectedMethod($manager, 'extractScriptsAndComments', [$versionInfo]);
$this->assertIsArray($comments);
$this->assertIsArray($scripts);
$this->assertEquals($expectedComments, $comments);
$this->assertEquals($expectedScripts, $scripts);
}
public function versionInfoProvider()
{
return [
[
'A single update comment string',
[
'A single update comment string'
],
[]
],
[
[
'A classic update comment string followed by script',
'update_script.php'
],
[
'A classic update comment string followed by script'
],
[
'update_script.php'
]
],
[
[
'scripts_can_go_first.php',
'An update comment string after the script',
],
[
'An update comment string after the script'
],
[
'scripts_can_go_first.php'
]
],
[
[
'scripts_can_go_first.php',
'An update comment string after the script',
'scripts_can_go_anywhere.php',
],
[
'An update comment string after the script'
],
[
'scripts_can_go_first.php',
'scripts_can_go_anywhere.php'
]
],
[
[
'scripts_can_go_first.php',
'The first update comment',
'scripts_can_go_anywhere.php',
'The second update comment',
],
[
'The first update comment',
'The second update comment'
],
[
'scripts_can_go_first.php',
'scripts_can_go_anywhere.php'
]
],
[
[
'file.name.with.dots.php',
'The first update comment',
'1.0.2.scripts_can_go_anywhere.php',
'The second update comment',
],
[
'The first update comment',
'The second update comment'
],
[
'file.name.with.dots.php',
'1.0.2.scripts_can_go_anywhere.php'
]
],
[
[
'subdirectory/file.name.with.dots.php',
'The first update comment',
'subdirectory\1.0.2.scripts_can_go_anywhere.php',
'The second update comment',
],
[
'The first update comment',
'The second update comment'
],
[
'subdirectory/file.name.with.dots.php',
'subdirectory\1.0.2.scripts_can_go_anywhere.php'
]
]
];
}
}

View File

@@ -0,0 +1,232 @@
<?php
namespace System\Tests\Classes\Asset;
use System\Classes\Asset\BundleManager;
use System\Tests\Bootstrap\TestCase;
class BundleManagerTest extends TestCase
{
protected BundleManager $bundleManager;
public function setUp(): void
{
parent::setUp();
// Reset the bundle manager
BundleManager::forgetInstance();
// Bind the instance for convenience
$this->bundleManager = BundleManager::instance();
}
/**
* Test the registerBundles & registerBundle functions correctly allow the user to append bundles
*/
public function testRegisterBundles(): void
{
$defaultBundles = $this->bundleManager->getBundles();
$this->bundleManager->registerBundles([
'winter1js' => [
'winter1js' => 'v1.0.0'
]
]);
$this->bundleManager->registerBundle('winter2js', [
'winter2js' => 'v1.0.0'
]);
$bundles = $this->bundleManager->getBundles();
$this->assertCount(count($defaultBundles) + 2, $bundles);
$this->assertContains('winter1js', $bundles);
$this->assertContains('winter2js', $bundles);
}
/**
* This test ensures that defining new bundles does not affect the default bundles
*/
public function testRegisterBundlesDoesNotBreakDefaults(): void
{
// Get the default bundles so we can validate them existing later
$defaultBundles = $this->bundleManager->getBundles();
// Flush the instance as if we have just booted
BundleManager::forgetInstance();
$this->bundleManager = BundleManager::instance();
// Register a new bundle
$this->bundleManager->registerBundles([
'winterjs' => [
'winterjs' => 'v1.0.0'
]
]);
// Grab the current bundles
$bundles = $this->bundleManager->getBundles();
// Validate that all default bundles have been registered when adding a new bundle
foreach ($defaultBundles as $defaultBundle) {
$this->assertContains($defaultBundle, $bundles);
}
}
/**
* Test the forcing a package to a new version
*/
public function testRegisterBundlesOverrideDefault(): void
{
$tailwindPackages = $this->bundleManager->getBundlePackages('tailwind', 'mix');
$this->bundleManager->registerBundle('tailwind', [
'tailwindcss' => 'dev-999.999.999',
'@tailwindcss/forms' => 'dev-999.999.999',
'@tailwindcss/typography' => 'dev-999.999.999',
]);
$updatedTailwindPackages = $this->bundleManager->getBundlePackages('tailwind', 'mix');
foreach ($tailwindPackages as $tailwindPackage => $version) {
$this->assertArrayHasKey($tailwindPackage, $updatedTailwindPackages);
$this->assertNotEquals($updatedTailwindPackages[$tailwindPackage], $version);
}
}
/**
* Test getting all supported bundles
*/
public function testGetBundles(): void
{
$bundles = $this->bundleManager->getBundles();
$this->assertIsArray($bundles);
$count = count($bundles);
$this->bundleManager->registerBundle('test', [
'a' => 'v0.1.2',
]);
$this->assertCount($count + 1, $this->bundleManager->getBundles());
}
/**
* Test getting bundle packages works and allows for config overloading
*/
public function testGetBundlePackages(): void
{
// Test that getting a default returns an array and validate one of the packages
$packages = $this->bundleManager->getBundlePackages('tailwind', 'mix');
$this->assertIsArray($packages);
$this->assertArrayHasKey('tailwindcss', $packages);
// Test that getting a package with compiler dependant packages does not return the package for invalid compiler
$packages = $this->bundleManager->getBundlePackages('vue', 'mix');
$this->assertIsArray($packages);
$this->assertArrayNotHasKey('@vitejs/plugin-vue', $packages);
// Test that getting a package with compiler dependant packages does return the package for a valid compiler
$packages = $this->bundleManager->getBundlePackages('vue', 'vite');
$this->assertIsArray($packages);
$this->assertArrayHasKey('@vitejs/plugin-vue', $packages);
// Validate that `testing` does not exist
$packages = $this->bundleManager->getBundlePackages('testing', 'vite');
$this->assertIsArray($packages);
$this->assertEmpty($packages);
$this->bundleManager->registerBundle('testing', [
'a' => 'v0.1.2',
'mix' => [
'b' => 'v0.1.3',
]
]);
// Validate the testing bundle works with compiler dependent packages
$packages = $this->bundleManager->getBundlePackages('testing', 'vite');
$this->assertIsArray($packages);
$this->assertCount(1, $packages);
$packages = $this->bundleManager->getBundlePackages('testing', 'mix');
$this->assertIsArray($packages);
$this->assertCount(2, $packages);
}
/**
* Test the AssetBundles setup handlers functionality
*/
public function testSetupHandlers(): void
{
// Test that the default handler is accessible
$handler = $this->bundleManager->getSetupHandler('tailwind');
$this->assertIsCallable($handler);
// Add a new handler
$this->bundleManager->registerSetupHandler('testing', fn () => true);
$handler = $this->bundleManager->getSetupHandler('testing');
$this->assertIsCallable($handler);
// Validate that handler returned is ours
$this->assertTrue($handler());
// Override the handler
$this->bundleManager->registerSetupHandler('testing', fn () => false);
$handler = $this->bundleManager->getSetupHandler('testing');
$this->assertIsCallable($handler);
$this->assertFalse($handler());
}
/**
* Test the AssetBundles scaffold handlers functionality
*/
public function testScaffoldHandlers(): void
{
// Test that the default handler is accessible
$handler = $this->bundleManager->getScaffoldHandler('tailwind');
$this->assertIsCallable($handler);
// Add a new handler
$this->bundleManager->registerScaffoldHandler('testing', fn () => true);
$handler = $this->bundleManager->getScaffoldHandler('testing');
$this->assertIsCallable($handler);
// Validate that handler returned is ours
$this->assertTrue($handler());
// Override the handler
$this->bundleManager->registerScaffoldHandler('testing', fn () => false);
$handler = $this->bundleManager->getScaffoldHandler('testing');
$this->assertIsCallable($handler);
$this->assertFalse($handler());
}
/**
* Validate that when registered a callable is added to the `callbacks` array within the BundleManager
*/
public function testRegisterCallback(): void
{
$this->expectExceptionMessage('callback registered');
$this->bundleManager->registerCallback(function (BundleManager $manager) {
throw new \RuntimeException('callback registered');
});
}
/**
* Test that calling registerBundles within registerCallback functions correctly
*/
public function testLoadRegisteredBundles(): void
{
$this->bundleManager->registerCallback(function (BundleManager $manager) {
$manager->registerBundles([
'winter-test-js' => [
'test-package' => 'v0.1.2',
'vite' => [
'vite-package' => 'v0.1.3',
]
]
]);
});
$this->assertContains('winter-test-js', $this->bundleManager->getBundles());
$bundle = $this->bundleManager->getBundlePackages('winter-test-js', 'mix');
$this->assertArrayHasKey('test-package', $bundle);
$this->assertArrayNotHasKey('vite-package', $bundle);
$bundle = $this->bundleManager->getBundlePackages('winter-test-js', 'vite');
$this->assertArrayHasKey('test-package', $bundle);
$this->assertArrayHasKey('vite-package', $bundle);
}
}

View File

@@ -0,0 +1,469 @@
<?php
namespace System\Tests\Classes\Asset;
use System\Classes\Asset\PackageJson;
use System\Tests\Bootstrap\TestCase;
class PackageJsonTest extends TestCase
{
/**
* Default test file fixture
*/
protected string $testFile;
/**
* Bind the default test file fixture
*/
public function setUp(): void
{
parent::setUp();
$this->testFile = __DIR__ . '/../../fixtures/npm/package-test.json';
}
/**
* Test loading a package.json file from path
*/
public function testLoadFile(): void
{
$packageJson = new PackageJson($this->testFile);
$contents = $packageJson->getContents();
$this->assertArrayHasKey('workspaces', $contents);
$this->assertArrayHasKey('packages', $contents['workspaces']);
$this->assertIsArray($contents['workspaces']['packages']);
}
/**
* Test loading a corrupted package.json file correctly errors
*/
public function testLoadCorruptFile(): void
{
$this->expectException(\JsonException::class);
new PackageJson(__DIR__ . '/../../fixtures/npm/package-corrupt.json');
}
/**
* Test creating an instance with non-existing file
*/
public function testNewFile(): void
{
$packageJson = new PackageJson(__DIR__ . '/../fixtures/npm/package-test-new.json');
$contents = $packageJson->getContents();
$this->assertIsArray($contents);
$this->assertCount(0, $contents);
}
/**
* Test creating an instance without a path
*/
public function testMemoryInstance(): void
{
$packageJson = new PackageJson();
$contents = $packageJson->getContents();
$this->assertIsArray($contents);
$this->assertCount(0, $contents);
}
/**
* Test setting and getting the name property
*/
public function testNameMethods(): void
{
$packageJson = new PackageJson();
$contents = $packageJson->getContents();
$this->assertIsArray($contents);
$this->assertCount(0, $contents);
$packageJson->setName('example-name');
$this->assertEquals('example-name', $packageJson->getName());
$contents = $packageJson->getContents();
$this->assertIsArray($contents);
$this->assertCount(1, $contents);
$this->assertArrayHasKey('name', $contents);
$this->assertEquals('example-name', $contents['name']);
}
/**
* Test getting the path of the current file
*/
public function testGetPath(): void
{
$packageJson = new PackageJson(__DIR__ . '/package.json');
$path = $packageJson->getPath();
$this->assertIsString($path);
$this->assertEquals(__DIR__ . '/package.json', $path);
$packageJson = new PackageJson();
$path = $packageJson->getPath();
$this->assertNull($path);
}
/**
* Test validating the name on set
*/
public function testNameValidation(): void
{
$packageJson = new PackageJson();
$this->assertThrows(function () use ($packageJson) {
$packageJson->setName('Test');
}, \InvalidArgumentException::class, 'Package names must be lower case');
$this->assertThrows(function () use ($packageJson) {
$packageJson->setName('.test');
}, \InvalidArgumentException::class, 'Package names must not start with . or _');
$this->assertThrows(function () use ($packageJson) {
$packageJson->setName('_test');
}, \InvalidArgumentException::class, 'Package names must not start with . or _');
$this->assertThrows(function () use ($packageJson) {
$packageJson->setName('te~st');
}, \InvalidArgumentException::class, 'Package names must not include special characters');
$this->assertThrows(function () use ($packageJson) {
$packageJson->setName('te*st');
}, \InvalidArgumentException::class, 'Package names must not include special characters');
$this->assertThrows(function () use ($packageJson) {
$packageJson->setName('test!');
}, \InvalidArgumentException::class, 'Package names must not include special characters');
$this->assertThrows(function () use ($packageJson) {
$packageJson->setName(sprintf('te%sst', str_repeat('s', 214)));
}, \InvalidArgumentException::class, 'Package names must not be longer than 214 characters');
$this->assertThrows(function () use ($packageJson) {
$packageJson->setName('test ');
}, \InvalidArgumentException::class, 'Package names must not include whitespace');
$this->assertThrows(function () use ($packageJson) {
$packageJson->setName(' test');
}, \InvalidArgumentException::class, 'Package names must not include whitespace');
}
/**
* Test checking package workspace exists
*/
public function testHasWorkspace(): void
{
$packageJson = new PackageJson($this->testFile);
$this->assertTrue($packageJson->hasWorkspace('themes/demo'));
$this->assertFalse($packageJson->hasWorkspace('themes/test'));
}
/**
* Test adding workspace package
*/
public function testAddWorkspace(): void
{
$packageJson = new PackageJson($this->testFile);
$this->assertFalse($packageJson->hasWorkspace('themes/test'));
$packageJson->addWorkspace('themes/test');
$this->assertTrue($packageJson->hasWorkspace('themes/test'));
// Create blank source
$packageJson = new PackageJson();
$this->assertFalse($packageJson->hasWorkspace('themes/test'));
$packageJson->addWorkspace('themes/test');
$this->assertTrue($packageJson->hasWorkspace('themes/test'));
}
/**
* Test removing workspace package
*/
public function testRemoveWorkspace(): void
{
$packageJson = new PackageJson($this->testFile);
$this->assertTrue($packageJson->hasWorkspace('themes/demo'));
$packageJson->removeWorkspace('themes/demo');
$this->assertFalse($packageJson->hasWorkspace('themes/demo'));
// Create blank source
$packageJson = new PackageJson();
$packageJson->removeWorkspace('themes/demo');
$this->assertFalse($packageJson->hasWorkspace('themes/demo'));
}
/**
* Test when adding a workspace package, it removes the package from ignored workspace packages
*/
public function testAddWorkspaceRemovesIgnoredPackage(): void
{
$packageJson = new PackageJson($this->testFile);
$this->assertFalse($packageJson->hasWorkspace('modules/backend'));
$this->assertTrue($packageJson->hasIgnoredPackage('modules/backend'));
$packageJson->addWorkspace('modules/backend');
$this->assertTrue($packageJson->hasWorkspace('modules/backend'));
$this->assertFalse($packageJson->hasIgnoredPackage('modules/backend'));
}
/**
* Test checking ignore package workspace exists
*/
public function testHasIgnoredPackage(): void
{
$packageJson = new PackageJson($this->testFile);
$this->assertTrue($packageJson->hasIgnoredPackage('modules/backend'));
$this->assertFalse($packageJson->hasIgnoredPackage('modules/test'));
// Create blank source
$packageJson = new PackageJson();
$this->assertFalse($packageJson->hasIgnoredPackage('modules/backend'));
$this->assertFalse($packageJson->hasIgnoredPackage('modules/test'));
}
/**
* Test adding ignore workspace package
*/
public function testAddIgnoredPackage(): void
{
$packageJson = new PackageJson($this->testFile);
$this->assertFalse($packageJson->hasIgnoredPackage('themes/example'));
$packageJson->addIgnoredPackage('themes/example');
$this->assertTrue($packageJson->hasIgnoredPackage('themes/example'));
// Create blank source
$packageJson = new PackageJson();
$packageJson->addIgnoredPackage('themes/example');
$this->assertTrue($packageJson->hasIgnoredPackage('themes/example'));
}
/**
* Test removing ignore workspace package
*/
public function testRemoveIgnoredPackage(): void
{
$packageJson = new PackageJson($this->testFile);
$this->assertTrue($packageJson->hasIgnoredPackage('modules/system'));
$packageJson->removeIgnoredPackage('modules/system');
$this->assertFalse($packageJson->hasIgnoredPackage('modules/system'));
// Create blank source
$packageJson = new PackageJson();
$packageJson->removeIgnoredPackage('modules/system');
$this->assertFalse($packageJson->hasIgnoredPackage('modules/system'));
}
/**
* Test when adding an ignore workspace package, it removes the package from workspace packages
*/
public function testAddIgnoredPackageRemovesWorkspace(): void
{
$packageJson = new PackageJson($this->testFile);
$this->assertTrue($packageJson->hasWorkspace('themes/demo'));
$this->assertFalse($packageJson->hasIgnoredPackage('themes/demo'));
$packageJson->addIgnoredPackage('themes/demo');
$this->assertFalse($packageJson->hasWorkspace('themes/demo'));
$this->assertTrue($packageJson->hasIgnoredPackage('themes/demo'));
}
/**
* Test checking if package.json has deps
*/
public function testHasDependency(): void
{
$packageJson = new PackageJson($this->testFile);
// Check that deps exist
$this->assertTrue($packageJson->hasDependency('test'));
$this->assertTrue($packageJson->hasDependency('test-dev'));
// Check that deps don't exist
$this->assertFalse($packageJson->hasWorkspace('test-dev2'));
$this->assertFalse($packageJson->hasWorkspace('testx'));
}
/**
* Test adding dependencies, when overwriting check that package is moved from devDeps to deps or revsersed
*
* @return void
*/
public function testAddDependency(): void
{
// Test adding packages
$packageJson = new PackageJson($this->testFile);
$packageJson->addDependency('winter', '4.0.1', dev: false)
->addDependency('winter-dev', '4.0.1', dev: true);
$this->assertArrayNotHasKey('winter', $packageJson->getContents()['devDependencies']);
$this->assertArrayHasKey('winter', $packageJson->getContents()['dependencies']);
$this->assertArrayNotHasKey('winter-dev', $packageJson->getContents()['dependencies']);
$this->assertArrayHasKey('winter-dev', $packageJson->getContents()['devDependencies']);
// Test adding packages with overwrites
$packageJson = new PackageJson($this->testFile);
// Should do nothing as test is already in deps not dev deps
$packageJson->addDependency('test', '6.0.1', dev: true, overwrite: false);
$this->assertArrayHasKey('test', $packageJson->getContents()['dependencies']);
$this->assertArrayNotHasKey('test', $packageJson->getContents()['devDependencies']);
$this->assertEquals('^1.0.0', $packageJson->getContents()['dependencies']['test']);
// Should move package from dev deps to deps with new version
$packageJson->addDependency('test', '6.0.1', dev: true, overwrite: true);
$this->assertArrayNotHasKey('test', $packageJson->getContents()['dependencies']);
$this->assertArrayHasKey('test', $packageJson->getContents()['devDependencies']);
$this->assertEquals('6.0.1', $packageJson->getContents()['devDependencies']['test']);
// Create blank source
$packageJson = new PackageJson();
// Add a non-dev dependency
$packageJson->addDependency('winter', '4.0.1', dev: false);
// Add a dev dependency
$packageJson->addDependency('winter-dev', '4.0.1', dev: true);
}
/**
* Test removing dependencies
*/
public function testRemoveDependency(): void
{
$packageJson = new PackageJson($this->testFile);
// Check package removed from dev deps
$this->assertArrayHasKey('test-dev', $packageJson->getContents()['devDependencies'] ?? []);
$packageJson->removeDependency('test-dev');
$this->assertArrayNotHasKey('test-dev', $packageJson->getContents()['devDependencies'] ?? []);
// Check package removed from deps
$this->assertArrayHasKey('test', $packageJson->getContents()['dependencies'] ?? []);
$packageJson->removeDependency('test');
$this->assertArrayNotHasKey('test', $packageJson->getContents()['dependencies'] ?? []);
// Create blank source
$packageJson = new PackageJson();
$packageJson->removeDependency('test-dev');
$this->assertArrayNotHasKey('test-dev', $packageJson->getContents()['devDependencies'] ?? []);
}
/**
* Test checking if a script exists in a package.json
*/
public function testHasScript(): void
{
$packageJson = new PackageJson($this->testFile);
$this->assertTrue($packageJson->hasScript('foo'));
$this->assertTrue($packageJson->hasScript('example'));
$this->assertTrue($packageJson->hasScript('test'));
$this->assertFalse($packageJson->hasScript('bar'));
$this->assertFalse($packageJson->hasScript('winter'));
$this->assertFalse($packageJson->hasScript('testing'));
}
/**
* Test getting the value of a script by name
*/
public function testGetScript(): void
{
$packageJson = new PackageJson($this->testFile);
$this->assertEquals('bar ./test', $packageJson->getScript('foo'));
$this->assertEquals('example test', $packageJson->getScript('example'));
$this->assertEquals('testing', $packageJson->getScript('test'));
$packageJson = new PackageJson();
$this->assertNull($packageJson->getScript('foo'));
}
/**
* Test getting the value of a script by name
*/
public function testAddScript(): void
{
$packageJson = new PackageJson($this->testFile);
$packageJson->addScript('winter', 'winter ./testing');
$this->assertTrue($packageJson->hasScript('winter'));
$this->assertEquals('winter ./testing', $packageJson->getScript('winter'));
$contents = $packageJson->getContents();
$this->assertTrue(isset($contents['scripts']['winter']));
$this->assertEquals('winter ./testing', $contents['scripts']['winter']);
$packageJson = new PackageJson();
$packageJson->addScript('winter', 'winter ./testing');
$this->assertTrue($packageJson->hasScript('winter'));
}
/**
* Test removing scripts from package.json
*/
public function testRemoveScript(): void
{
$packageJson = new PackageJson($this->testFile);
$this->assertTrue($packageJson->hasScript('foo'));
$packageJson->removeScript('foo');
$this->assertFalse($packageJson->hasScript('foo'));
$this->assertTrue($packageJson->hasScript('example'));
$packageJson->removeScript('example');
$this->assertFalse($packageJson->hasScript('example'));
$packageJson = new PackageJson();
$packageJson->removeScript('foo');
$this->assertFalse($packageJson->hasScript('foo'));
}
/**
* Test saving, when saving with a file path set on init and passing a file path on save. Fails when no path given
*/
public function testSave(): void
{
$srcFile = $this->testFile;
$backupFile = __DIR__ . '/../../fixtures/npm/package-test.json.back';
// Backup config file
copy($srcFile, $backupFile);
// Make a change and save the file, overwriting
$packageJson = new PackageJson($srcFile);
$this->assertNull($packageJson->getName());
$packageJson->setName('testing');
$packageJson->save();
// Validate overwrite worked
$packageJson = new PackageJson($srcFile);
$this->assertEquals('testing', $packageJson->getName());
// Restore the config file
copy($backupFile, $srcFile);
// Remove backup file
unlink($backupFile);
// Test saving file to new path
$testFile = __DIR__ . '/../../fixtures/npm/package-test.json.test';
$packageJson = new PackageJson($srcFile);
$this->assertNull($packageJson->getName());
$packageJson->setName('testing');
$packageJson->save($testFile);
// Validate new file path exists and contains change
$packageJson = new PackageJson($testFile);
$this->assertEquals('testing', $packageJson->getName());
// Remove test file
unlink($testFile);
// Validate that a file save with no path throws an error
$this->assertThrows(function () {
$packageJson = new PackageJson();
$packageJson->setName('should-fail');
$packageJson->save();
}, \RuntimeException::class, 'Unable to save, no path given');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace System\Tests\Console;
use File;
use InvalidArgumentException;
use System\Tests\Bootstrap\TestCase;
class CreateCommandTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->app->setPluginsPath(base_path() . '/modules/system/tests/fixtures/plugins/');
}
public function testCreatingCommand()
{
$this->artisan('create:command Winter.Tester TestCommand')
->assertExitCode(0);
$this->artisan('create:command Winter.Tester Test1Command')
->assertExitCode(0);
$this->artisan('create:command Winter.Tester4You TestCommand')
->assertExitCode(0);
$this->artisan('create:command Winter.Tester4You Test1Command')
->assertExitCode(0);
}
public function testNotCreatingCommandBeginingWithNumber()
{
$this->artisan('create:command Winter.Tester 1Command')->assertFailed();
$this->artisan('create:command Winter.Tester4You 1Command')->assertFailed();
}
protected function tearDown(): void
{
File::deleteDirectory(plugins_path('winter/tester/console'));
File::deleteDirectory(plugins_path('winter/tester4you'));
parent::tearDown();
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace System\Tests\Console;
use File;
use Illuminate\Database\Schema\Blueprint;
use Schema;
use System\Tests\Bootstrap\PluginTestCase;
class CreateMigrationTest extends PluginTestCase
{
public function setUp(): void
{
parent::setUp();
$this->app->setPluginsPath(base_path() . '/modules/system/tests/fixtures/plugins/');
$this->table = 'winter_tester_test_model';
$this->versionFile = plugins_path('winter/tester/updates/version.yaml');
$this->versionFolder = plugins_path('winter/tester/updates/v0.0.1');
File::copy($this->versionFile, $this->versionFile . '.bak');
}
public function testCreateMigration()
{
$this->artisan('create:migration Winter.Tester -c --force --for-version v0.0.1 --model TestModel --name create_table');
$this->assertFileExists($this->versionFolder . '/create_table.php');
$migration = require_once $this->versionFolder . '/create_table.php';
$migration->up();
$this->assertTrue(Schema::hasTable($this->table));
$columns = [
'id' => ['type'=>'integer', 'index'=>'primary', 'required'=>true],
'cb' => ['type'=>'boolean'],
'switch' => ['type'=>'boolean'],
'int' => ['type'=>'integer'],
'uint' => ['type'=>'integer', 'required'=>true],
'double' => ['type'=>'float'],
'range' => ['type'=>'integer', 'required'=>true],
'datetime' => ['type'=>'datetime'],
'date' => ['type'=>'date', 'required'=>true],
'time' => ['type'=>'time'],
'md' => ['type'=>'text'],
'textarea' => ['type'=>'text'],
'text' => ['type'=>'string', 'required'=>true],
'phone_id' => ['type'=>'integer', 'index'=>true, 'required'=>true],
'user_id' => ['type'=>'integer', 'index'=>true, 'required'=>true],
'data' => ['type'=>'text'],
'sort_order' => ['type'=>'integer', 'index'=>true],
'taggable_id' => ['type'=>'integer', 'index'=>'morphable_index'],
'taggable_type' => ['type'=>'string', 'index'=>'morphable_index'],
'created_at' => ['type'=>'datetime'],
'updated_at' => ['type'=>'datetime'],
];
$table = Schema::getConnection()->getDoctrineSchemaManager()->listTableDetails($this->table);
foreach ($columns as $name => $definition) {
$this->assertEquals(array_get($definition, 'type'), Schema::getColumnType($this->table, $name));
// assert an index has been created for the primary, morph and foreign keys
if ($indexName = array_get($definition, 'index')) {
if ($indexName === true) {
$indexName = sprintf("%s_%s_index", $this->table, $name);
}
$this->assertTrue($table->hasIndex($indexName));
if ($indexName === 'morphable_index') {
$index = $table->getIndex($indexName);
$this->assertTrue(in_array($name, $index->getColumns()));
}
}
$this->assertEquals(array_get($definition, 'required', false), $table->getColumn($name)->getNotnull());
}
$migration->down();
$this->assertFalse(Schema::hasTable($this->table));
}
public function tearDown(): void
{
File::move($this->versionFile . '.bak', $this->versionFile);
File::deleteDirectory($this->versionFolder);
parent::tearDown();
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace System\Tests\Console;
use System\Tests\Bootstrap\TestCase;
use Winter\Storm\Foundation\Bootstrap\LoadConfiguration;
class WinterEnvTest extends TestCase
{
/** @var bool If the config fixtures have been copied */
public static $fixturesCopied = false;
/** @var string Stores the original config path from the app container */
public static $origConfigPath;
/** @var string Stores the original environment path from the app container */
public static $origEnvPath;
protected function setUp(): void
{
parent::setUp();
$this->setUpConfigFixtures();
}
public function testCommand()
{
$this->artisan('winter:env')
->assertExitCode(0);
// Check environment file
$envFile = file_get_contents($this->app->environmentFilePath());
$this->assertStringContainsString('APP_DEBUG=true', $envFile);
$this->assertStringContainsString('APP_URL="https://env-test.localhost"', $envFile);
$this->assertStringContainsString('DB_CONNECTION="mysql"', $envFile);
$this->assertStringContainsString('DB_DATABASE="data#base"', $envFile);
$this->assertStringContainsString('DB_USERNAME="teal\'c"', $envFile);
$this->assertStringContainsString('DB_PASSWORD="test\\"quotes\'test"', $envFile);
$this->assertStringContainsString('DB_PORT=3306', $envFile);
// Check app.php config file
$appConfigFile = file_get_contents(storage_path('temp/tests/config/app.php'));
$this->assertStringContainsString('\'debug\' => env(\'APP_DEBUG\', true),', $appConfigFile);
$this->assertStringContainsString('\'url\' => env(\'APP_URL\', \'https://env-test.localhost\'),', $appConfigFile);
// Check database.php config file
$appConfigFile = file_get_contents(storage_path('temp/tests/config/database.php'));
$this->assertStringContainsString('\'default\' => env(\'DB_CONNECTION\', \'mysql\')', $appConfigFile);
$this->assertStringContainsString('\'port\' => env(\'DB_PORT\', 3306),', $appConfigFile);
// Both the following configurations had values in the original config, they should be stripped out once
// the .env file is generated.
$this->assertStringContainsString('\'username\' => env(\'DB_USERNAME\', \'\'),', $appConfigFile);
$this->assertStringContainsString('\'password\' => env(\'DB_PASSWORD\', \'\'),', $appConfigFile);
}
protected function tearDown(): void
{
$this->tearDownConfigFixtures();
parent::tearDown();
}
protected function setUpConfigFixtures()
{
// Mock config path and copy fixtures
if (!is_dir(storage_path('temp/tests/config'))) {
mkdir(storage_path('temp/tests/config'), 0777, true);
}
if (!is_dir(storage_path('temp/tests/env'))) {
mkdir(storage_path('temp/tests/env'), 0777, true);
}
foreach (glob(base_path('modules/system/tests/fixtures/config/*.php')) as $file) {
$path = pathinfo($file);
copy($file, storage_path('temp/tests/config/' . $path['basename']));
}
static::$fixturesCopied = true;
// Store original config path
static::$origConfigPath = $this->app->make('path.config');
static::$origEnvPath = $this->app->environmentPath();
$this->app->instance('path.config', storage_path('temp/tests/config'));
$this->app->useEnvironmentPath(storage_path('temp/tests/env'));
// Re-load configuration
$configBootstrap = new LoadConfiguration;
$configBootstrap->bootstrap($this->app);
}
protected function tearDownConfigFixtures()
{
// Remove copied config fixtures
if (static::$fixturesCopied) {
foreach (glob(storage_path('temp/tests/config/*.php')) as $file) {
unlink($file);
}
rmdir(storage_path('temp/tests/config'));
unlink(storage_path('temp/tests/env/.env'));
rmdir(storage_path('temp/tests/env'));
rmdir(storage_path('temp/tests'));
static::$fixturesCopied = false;
}
// Restore config path
if (self::$origConfigPath) {
$this->app->instance('path.config', static::$origConfigPath);
static::$origConfigPath = null;
}
// Restore environment path
if (self::$origEnvPath) {
$this->app->useEnvironmentPath(static::$origEnvPath);
static::$origEnvPath = null;
}
// Re-load configuration
$configBootstrap = new LoadConfiguration;
$configBootstrap->bootstrap($this->app);
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace System\Tests\Console;
use System\Tests\Bootstrap\TestCase;
class WinterUtilTest extends TestCase
{
protected string $defaultClient;
protected string $langClient;
protected string $langCountryClient;
protected array $createdDirs = [];
protected function setUp(): void
{
parent::setUp();
$this->defaultClient = base_path('modules/system/lang/en/client.php');
$this->langClient = base_path('lang/en/system/client.php');
$this->langCountryClient = base_path('lang/en-gb/system/client.php');
// Backup original files
if (file_exists($this->defaultClient)) {
rename($this->defaultClient, $this->defaultClient . '.backup');
}
if (file_exists($this->langClient)) {
rename($this->langClient, $this->langClient . '.backup');
}
if (file_exists($this->langCountryClient)) {
rename($this->langCountryClient, $this->langCountryClient . '.backup');
}
}
public function testCompileLang()
{
file_put_contents($this->defaultClient, '<?php return [\'winter\' => \'is coming\'];');
// execute compile
$this->artisan('winter:util compile lang')->execute();
// validate default lang handling
$lang = file_get_contents(base_path('modules/system/assets/js/lang/lang.en.js'));
$this->assertStringContainsString('winter', $lang);
$this->assertStringContainsString('is coming', $lang);
// simulate override
$this->createdDirs = [];
foreach (['lang/en/system', 'lang/en-gb/system'] as $slug) {
$path = rtrim(base_path(), '/');
foreach (explode('/', $slug) as $dir) {
$path = $path . '/' . $dir;
if (!is_dir($path)) {
mkdir($path, 0755);
$this->createdDirs[] = $path;
}
}
}
file_put_contents($this->langClient, '<?php return [\'winter\' => \'is epic\'];');
file_put_contents($this->langCountryClient, '<?php return [\'whats_epic\' => \'winter\'];');
// execute compile
$this->artisan('winter:util compile lang')->execute();
// validate override handling
$lang = file_get_contents(base_path('modules/system/assets/js/lang/lang.en.js'));
$this->assertStringContainsString('winter', $lang);
$this->assertStringContainsString('is epic', $lang);
// check that lang subset has included parent overrides
$lang = file_get_contents(base_path('modules/system/assets/js/lang/lang.en-gb.js'));
$this->assertStringContainsString('winter', $lang);
$this->assertStringContainsString('is epic', $lang);
$this->assertStringContainsString('whats_epic', $lang);
$this->assertStringContainsString('winter', $lang);
}
protected function tearDown(): void
{
unlink($this->defaultClient);
rename($this->defaultClient . '.backup', $this->defaultClient);
foreach ([$this->langClient, $this->langCountryClient] as $client) {
unlink($client);
if (file_exists($client . '.backup')) {
rename($client . '.backup', $client);
}
}
foreach (array_reverse($this->createdDirs) as $dir) {
rmdir($dir);
}
$this->artisan('winter:util compile lang')->execute();
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace System\Tests\Console\Asset;
trait NpmTestTrait
{
protected string $jsonPath;
protected string $backupPath;
/**
* Helper to run test logic and handle restoring package.json file after
*/
protected function withPackageJsonRestore(callable $callback): void
{
copy($this->jsonPath, $this->backupPath);
$callback();
rename($this->backupPath, $this->jsonPath);
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace System\Tests\Console\Asset\Mix;
use System\Tests\Bootstrap\TestCase;
use Winter\Storm\Support\Facades\File;
class MixCompileTest extends TestCase
{
protected string $command = 'mix:compile';
public function setUp(): void
{
parent::setUp();
if (!File::exists(base_path('node_modules'))) {
$this->markTestSkipped('This test requires node_modules to be installed');
}
if (!File::exists(base_path('node_modules/.bin/mix'))) {
$this->markTestSkipped('This test requires the mix package to be installed');
}
}
public function testCompileMultiple()
{
$this->artisan($this->command, [
'--manifest' => 'modules/system/tests/fixtures/npm/package-ac.json',
'--silent' => true
])->assertExitCode(0);
$this->assertFileExists(base_path('modules/system/tests/fixtures/plugins/mix/testa/assets/dist/app.js'));
$this->assertFileExists(base_path('modules/system/tests/fixtures/plugins/mix/testc/assets/dist/app.js'));
}
public function testCompileMultipleWithErrors()
{
$this->artisan($this->command, [
'--manifest' => 'modules/system/tests/fixtures/npm/package-abc.json',
'--disable-tty' => true
])
->expectsOutputToContain('Error: Can\'t resolve \'some-missing-package\'')
->assertExitCode(1);
$this->assertFileExists(base_path('modules/system/tests/fixtures/plugins/mix/testa/assets/dist/app.js'));
$this->assertFileExists(base_path('modules/system/tests/fixtures/plugins/mix/testb/assets/dist/app.js'));
$this->assertFileExists(base_path('modules/system/tests/fixtures/plugins/mix/testc/assets/dist/app.js'));
}
public function testCompileTarget()
{
$this->artisan($this->command, [
'--manifest' => 'modules/system/tests/fixtures/npm/package-abc.json',
'--package' => 'mix.testa',
'--silent' => true
])->assertExitCode(0);
$this->assertFileExists(base_path('modules/system/tests/fixtures/plugins/mix/testa/assets/dist/app.js'));
$this->assertFileNotExists(base_path('modules/system/tests/fixtures/plugins/mix/testb/assets/dist/app.js'));
$this->assertFileNotExists(base_path('modules/system/tests/fixtures/plugins/mix/testc/assets/dist/app.js'));
}
public function testCompileTargetWithError()
{
$this->artisan($this->command, [
'--manifest' => 'modules/system/tests/fixtures/npm/package-abc.json',
'--package' => 'mix.testb',
'--disable-tty' => true
])
->expectsOutputToContain('Error: Can\'t resolve \'some-missing-package\'')
->assertExitCode(1);
$this->assertFileNotExists(base_path('modules/system/tests/fixtures/plugins/mix/testa/assets/dist/app.js'));
$this->assertFileNotExists(base_path('modules/system/tests/fixtures/plugins/mix/testc/assets/dist/app.js'));
$this->assertFileExists(base_path('modules/system/tests/fixtures/plugins/mix/testb/assets/dist/app.js'));
}
public function testCompileTargetStopOnError()
{
$this->artisan($this->command, [
'--manifest' => 'modules/system/tests/fixtures/npm/package-abc.json',
'--stop-on-error' => true,
'--disable-tty' => true
])
->expectsOutputToContain('Error: Can\'t resolve \'some-missing-package\'')
->assertExitCode(1);
$this->assertFileExists(base_path('modules/system/tests/fixtures/plugins/mix/testa/assets/dist/app.js'));
$this->assertFileExists(base_path('modules/system/tests/fixtures/plugins/mix/testb/assets/dist/app.js'));
$this->assertFileNotExists(base_path('modules/system/tests/fixtures/plugins/mix/testc/assets/dist/app.js'));
}
public function tearDown(): void
{
File::deleteDirectory('modules/system/tests/fixtures/plugins/mix/testa/assets/dist');
File::deleteDirectory('modules/system/tests/fixtures/plugins/mix/testb/assets/dist');
File::deleteDirectory('modules/system/tests/fixtures/plugins/mix/testc/assets/dist');
parent::tearDown();
}
}

View File

@@ -0,0 +1,234 @@
<?php
namespace System\Tests\Console\Asset\Mix;
use System\Classes\PluginManager;
use System\Tests\Bootstrap\TestCase;
use Winter\Storm\Support\Facades\File;
class MixCreateTest extends TestCase
{
protected string $testPlugin = 'Winter.Sample';
public function testConfigWritten(): void
{
$path = PluginManager::instance()->findByIdentifier($this->testPlugin)->getPluginPath();
$configPath = $path . '/winter.mix.js';
// Check file does not exist
$this->assertFileNotExists($configPath);
// Run the config command to generate the vite config
$this->artisan('mix:create', [
'packageName' => $this->testPlugin,
'--no-stubs' => true,
])
->doesntExpectOutput('winter.mix.js already exists, overwrite?')
->assertExitCode(0);
// Validate the manifest was written
$this->assertFileExists($configPath);
// Get the predicted contents
$fixture = str_replace(
'{{packageName}}',
'winter-sample',
File::get(base_path('modules/system/console/asset/fixtures/config/mix/winter.mix.js.fixture')),
);
// Check the file written is what was expected
$this->assertEquals($fixture, File::get($configPath));
// Overwrite the file content
File::put($configPath, 'testing');
// Check that refusing to overwrite does not replace file contents
$this->artisan('mix:create', [
'packageName' => $this->testPlugin,
'--no-stubs' => true,
])
->expectsQuestion('winter.mix.js already exists, overwrite?', false)
->assertExitCode(0);
// Check file contents was not overwritten
$this->assertNotEquals($fixture, File::get($configPath));
// Run command confirming to overwrite file contents works
$this->artisan('mix:create', [
'packageName' => $this->testPlugin,
'--no-stubs' => true,
])
->expectsQuestion('winter.mix.js already exists, overwrite?', true)
->assertExitCode(0);
// Check file contents was overwritten
$this->assertEquals($fixture, File::get($configPath));
}
public function testConfigTailwind(): void
{
$path = PluginManager::instance()->findByIdentifier($this->testPlugin)->getPluginPath();
$configPath = $path . '/winter.mix.js';
$packageJson = $path . '/package.json';
// Check file does not exist
$this->assertFileNotExists($configPath);
// Run the config command to generate the vite config
$this->artisan('mix:create', [
'packageName' => $this->testPlugin,
'--tailwind' => true,
'--no-stubs' => true,
])
->assertExitCode(0);
// Check files are created correctly
$this->assertFileExists($configPath);
$this->assertFileExists($path . '/tailwind.config.js');
$this->assertFileExists($path . '/postcss.config.mjs');
// Get the contents of the package.json
$json = json_decode(File::get($packageJson));
// Check tailwindcss is required
$this->assertTrue(isset($json->devDependencies->tailwindcss));
}
public function testConfigReact(): void
{
$path = PluginManager::instance()->findByIdentifier($this->testPlugin)->getPluginPath();
$configPath = $path . '/winter.mix.js';
$packageJson = $path . '/package.json';
// Check file does not exist
$this->assertFileNotExists($configPath);
// Run the config command to generate the vite config with react
$this->artisan('mix:create', [
'packageName' => $this->testPlugin,
'--react' => true,
'--no-stubs' => true,
])
->assertExitCode(0);
// Check files are created correctly
$this->assertFileExists($configPath);
$this->assertFileExists($packageJson);
// Get the contents of the package.json
$json = json_decode(File::get($packageJson));
// Check react is required
$this->assertTrue(isset($json->devDependencies->react));
}
public function testConfigTailwindReact(): void
{
$path = PluginManager::instance()->findByIdentifier($this->testPlugin)->getPluginPath();
$configPath = $path . '/winter.mix.js';
$packageJson = $path . '/package.json';
// Check file does not exist
$this->assertFileNotExists($configPath);
// Run the config command to generate the vite config with react
$this->artisan('mix:create', [
'packageName' => $this->testPlugin,
'--tailwind' => true,
'--react' => true,
'--no-stubs' => true,
])
->assertExitCode(0);
// Check files are created correctly
$this->assertFileExists($configPath);
$this->assertFileExists($packageJson);
$this->assertFileExists($path . '/tailwind.config.js');
$this->assertFileExists($path . '/postcss.config.mjs');
// Get the contents of the package.json
$json = json_decode(File::get($packageJson));
// Check tailwind & react are required
$this->assertTrue(isset($json->devDependencies->tailwindcss));
$this->assertTrue(isset($json->devDependencies->react));
}
public function testConfigVue(): void
{
$path = PluginManager::instance()->findByIdentifier($this->testPlugin)->getPluginPath();
$configPath = $path . '/winter.mix.js';
$packageJson = $path . '/package.json';
// Check file does not exist
$this->assertFileNotExists($configPath);
// Run the config command to generate the vite config with vue
$this->artisan('mix:create', [
'packageName' => $this->testPlugin,
'--vue' => true,
'--no-stubs' => true,
])
->assertExitCode(0);
// Check files are created correctly
$this->assertFileExists($configPath);
$this->assertFileExists($packageJson);
// Get the contents of the package.json
$json = json_decode(File::get($packageJson));
// Check vue is required
$this->assertTrue(isset($json->devDependencies->vue));
}
public function testConfigTailwindVue(): void
{
$path = PluginManager::instance()->findByIdentifier($this->testPlugin)->getPluginPath();
$configPath = $path . '/winter.mix.js';
$packageJson = $path . '/package.json';
// Check file does not exist
$this->assertFileNotExists($configPath);
// Run the config command to generate the vite config with vue
$this->artisan('mix:create', [
'packageName' => $this->testPlugin,
'--tailwind' => true,
'--vue' => true,
'--no-stubs' => true,
])
->assertExitCode(0);
// Check files are created correctly
$this->assertFileExists($configPath);
$this->assertFileExists($packageJson);
$this->assertFileExists($path . '/tailwind.config.js');
$this->assertFileExists($path . '/postcss.config.mjs');
// Get the contents of the package.json
$json = json_decode(File::get($packageJson));
// Check tailwind & vue are required
$this->assertTrue(isset($json->devDependencies->tailwindcss));
$this->assertTrue(isset($json->devDependencies->vue));
}
public function tearDown(): void
{
$path = PluginManager::instance()->findByIdentifier($this->testPlugin)->getPluginPath();
$files = [
$path . '/winter.mix.js',
$path . '/package.json',
$path . '/tailwind.config.js',
$path . '/postcss.config.mjs',
];
foreach ($files as $file) {
if (File::exists($file)) {
File::delete($file);
}
}
}
}

View File

@@ -0,0 +1,213 @@
<?php
namespace System\Tests\Console\Asset\Mix;
use System\Classes\Asset\PackageManager;
use System\Tests\Bootstrap\TestCase;
use Winter\Storm\Exception\SystemException;
use Winter\Storm\Support\Facades\File;
class MixInstallTest extends TestCase
{
protected string $fixturePath;
protected string $jsonPath;
protected string $lockPath;
protected string $backupPath;
public function setUp(): void
{
parent::setUp();
if (!File::exists(base_path('node_modules'))) {
$this->markTestSkipped('This test requires node_modules to be installed');
}
// Define some helpful paths
$this->fixturePath = base_path('modules/system/tests');
$this->jsonPath = $this->fixturePath . '/package.json';
$this->lockPath = $this->fixturePath . '/package-lock.json';
$this->backupPath = $this->fixturePath . '/package-testing.json';
// Add our testing theme because it won't be auto discovered
PackageManager::instance()->registerPackage(
'theme-assettest',
base_path('modules/system/tests/fixtures/themes/assettest/winter.mix.js'),
'mix'
);
PackageManager::instance()->registerPackage(
'theme-npmtest',
base_path('modules/system/tests/fixtures/themes/npmtest/winter.mix.js'),
'mix'
);
}
public function testMixInstallMissingPackageJson(): void
{
$this->artisan('mix:install', [
'assetPackage' => ['theme-assettest'],
'--package-json' => '/some/file',
'--no-install' => true
])
->expectsOutputToContain('The supplied --package-json path does not exist.')
->assertExitCode(1);
}
public function testMixInstallSinglePackage(): void
{
$this->withPackageJsonRestore(function () {
$this->artisan('mix:install', [
'assetPackage' => ['theme-assettest'],
'--package-json' => $this->jsonPath,
'--no-install' => true
])
->expectsQuestion('laravel-mix was not found as a dependency in package.json, would you like to add it?', true)
->expectsOutput('Adding theme-assettest (modules/system/tests/fixtures/themes/assettest) to the workspaces.packages property in package.json')
->assertExitCode(0);
$this->assertFileExists($this->jsonPath);
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayHasKey('devDependencies', $packageJson);
$this->assertArrayHasKey('laravel-mix', $packageJson['devDependencies']);
$this->assertArrayHasKey('workspaces', $packageJson);
$this->assertArrayHasKey('packages', $packageJson['workspaces']);
$this->assertContains('modules/system/tests/fixtures/themes/assettest', $packageJson['workspaces']['packages']);
});
}
public function testMixInstallMultiplePackages(): void
{
$this->withPackageJsonRestore(function () {
$this->artisan('mix:install', [
'assetPackage' => ['theme-assettest', 'theme-npmtest'],
'--package-json' => $this->jsonPath,
'--no-install' => true
])
->expectsQuestion('laravel-mix was not found as a dependency in package.json, would you like to add it?', true)
->expectsOutput('Adding theme-assettest (modules/system/tests/fixtures/themes/assettest) to the workspaces.packages property in package.json')
->expectsOutput('Adding theme-npmtest (modules/system/tests/fixtures/themes/npmtest) to the workspaces.packages property in package.json')
->assertExitCode(0);
$this->assertFileExists($this->jsonPath);
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayHasKey('devDependencies', $packageJson);
$this->assertArrayHasKey('laravel-mix', $packageJson['devDependencies']);
$this->assertArrayHasKey('workspaces', $packageJson);
$this->assertArrayHasKey('packages', $packageJson['workspaces']);
$this->assertContains('modules/system/tests/fixtures/themes/assettest', $packageJson['workspaces']['packages']);
$this->assertContains('modules/system/tests/fixtures/themes/npmtest', $packageJson['workspaces']['packages']);
});
}
public function testMixInstallMissingPackage(): void
{
// We should receive an exception for a missing package
$this->withPackageJsonRestore(function () {
$this->expectException(SystemException::class);
$this->artisan('mix:install', [
'assetPackage' => ['theme-assettest2'],
'--package-json' => $this->jsonPath,
'--no-install' => true
])
->assertExitCode(1);
});
}
public function testMixInstallRelativePath(): void
{
$this->withPackageJsonRestore(function () {
$this->artisan('mix:install', [
'assetPackage' => ['theme-assettest'],
'--package-json' => 'modules/system/tests/package.json',
'--no-install' => true
])
->expectsQuestion('laravel-mix was not found as a dependency in package.json, would you like to add it?', true)
->expectsOutput('Adding theme-assettest (modules/system/tests/fixtures/themes/assettest) to the workspaces.packages property in package.json')
->assertExitCode(0);
});
}
public function testMixInstallIgnoredPackage(): void
{
$this->withPackageJsonRestore(function () {
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$packageJson['workspaces'] = [
'ignoredPackages' => [
'modules/system/tests/fixtures/themes/assettest'
]
];
File::put($this->jsonPath, json_encode($packageJson, JSON_PRETTY_PRINT));
$this->artisan('mix:install', [
'assetPackage' => ['theme-assettest'],
'--package-json' => $this->jsonPath,
'--no-install' => true
])
->expectsQuestion('laravel-mix was not found as a dependency in package.json, would you like to add it?', false)
->expectsOutput('The requested package theme-assettest (modules/system/tests/fixtures/themes/assettest) is ignored, remove it from package.json to continue.')
->assertExitCode(0);
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayHasKey('workspaces', $packageJson);
$this->assertArrayNotHasKey('packages', $packageJson['workspaces']);
$this->assertArrayNotHasKey('dependencies', $packageJson);
$this->assertArrayNotHasKey('devDependencies', $packageJson);
});
}
public function testMixInstallWithNpmInstall(): void
{
$this->withPackageJsonRestore(function () {
$this->assertDirectoryDoesNotExist($this->fixturePath . '/node_modules');
$this->artisan('mix:install', [
'assetPackage' => ['theme-assettest'],
'--package-json' => $this->jsonPath,
'--disable-tty' => true
])
->expectsQuestion('laravel-mix was not found as a dependency in package.json, would you like to add it?', true)
->expectsOutput('Adding theme-assettest (modules/system/tests/fixtures/themes/assettest) to the workspaces.packages property in package.json')
->expectsOutputToContain('packages, and audited') // output from npm i
->assertExitCode(0);
$this->assertFileExists($this->jsonPath);
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayHasKey('devDependencies', $packageJson);
$this->assertArrayHasKey('laravel-mix', $packageJson['devDependencies']);
$this->assertArrayHasKey('workspaces', $packageJson);
$this->assertArrayHasKey('packages', $packageJson['workspaces']);
$this->assertContains('modules/system/tests/fixtures/themes/assettest', $packageJson['workspaces']['packages']);
$this->assertFileExists($this->lockPath);
$this->assertDirectoryExists($this->fixturePath . '/node_modules');
$this->assertDirectoryExists($this->fixturePath . '/node_modules/laravel-mix');
});
}
/**
* Helper to run test logic and handle restoring package.json file after
*/
protected function withPackageJsonRestore(callable $callback): void
{
File::copy($this->backupPath, $this->jsonPath);
$callback();
File::delete($this->jsonPath);
}
public function tearDown(): void
{
if (File::isDirectory($this->fixturePath . '/node_modules')) {
File::deleteDirectory($this->fixturePath . '/node_modules');
}
if (File::exists($this->lockPath)) {
File::delete($this->lockPath);
}
parent::tearDown();
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace System\Tests\Console\Asset\Npm;
use System\Classes\Asset\PackageManager;
use System\Tests\Bootstrap\TestCase;
use System\Tests\Console\Asset\NpmTestTrait;
use Winter\Storm\Support\Facades\File;
class NpmInstallTest extends TestCase
{
use NpmTestTrait;
protected string $themePath;
protected string $jsonPath;
protected string $lockPath;
protected string $backupPath;
public function setUp(): void
{
parent::setUp();
if (!File::exists(base_path('node_modules'))) {
$this->markTestSkipped('This test requires node_modules to be installed');
}
// Define some helpful paths
$this->themePath = base_path('modules/system/tests/fixtures/themes/npmtest');
$this->jsonPath = $this->themePath . '/package.json';
$this->lockPath = $this->themePath . '/package-lock.json';
$this->backupPath = $this->themePath . '/package.backup.json';
// Add our testing theme because it won't be auto discovered
PackageManager::instance()->registerPackage(
'theme-npmtest',
$this->themePath . '/vite.config.mjs',
'vite'
);
}
/**
* Test the ability to install a single npm package via artisan
*/
public function testNpmInstallSingle(): void
{
// Validate the package Json does not have dependencies
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayNotHasKey('dependencies', $packageJson);
// Validate node_modules not found
$this->assertDirectoryDoesNotExist($this->themePath . '/node_modules');
$this->assertFileNotExists($this->lockPath);
$this->withPackageJsonRestore(function () {
// Run npm install for a single non-dev package
$this->artisan('npm:install', [
'package' => 'theme-npmtest',
'npmArgs' => ['is-odd'],
'--disable-tty' => true
])
->assertExitCode(0);
// Validate lock file was generated successfully
$this->assertFileExists($this->lockPath);
// Validate the contents of package.json
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayHasKey('dependencies', $packageJson);
$this->assertArrayHasKey('is-odd', $packageJson['dependencies']);
// Validate that node_modules paths exist
$this->assertDirectoryExists($this->themePath . '/node_modules');
$this->assertDirectoryExists($this->themePath . '/node_modules/is-odd');
});
}
/**
* Test the ability to install multiple npm packages via artisan
*/
public function testNpmInstallMultiple(): void
{
// Validate the package Json does not have dependencies
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayNotHasKey('dependencies', $packageJson);
// Validate node_modules not found
$this->assertDirectoryDoesNotExist($this->themePath . '/node_modules');
$this->assertFileNotExists($this->lockPath);
$this->withPackageJsonRestore(function () {
// Run npm install for multiple non-dev packages
$this->artisan('npm:install', [
'package' => 'theme-npmtest',
'npmArgs' => ['is-odd', 'is-even'],
'--disable-tty' => true
])
->assertExitCode(0);
// Validate lock file was generated successfully
$this->assertFileExists($this->lockPath);
// Validate the contents of package.json
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayHasKey('dependencies', $packageJson);
$this->assertArrayHasKey('is-odd', $packageJson['dependencies']);
$this->assertArrayHasKey('is-even', $packageJson['dependencies']);
// Validate that node_modules paths exist
$this->assertDirectoryExists($this->themePath . '/node_modules');
$this->assertDirectoryExists($this->themePath . '/node_modules/is-odd');
$this->assertDirectoryExists($this->themePath . '/node_modules/is-even');
});
}
/**
* Test the ability to install a single dev npm package via artisan
*/
public function testNpmInstallSingleDev(): void
{
// Validate the package Json does not have dependencies
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayNotHasKey('devDependencies', $packageJson);
// Validate node_modules not found
$this->assertDirectoryDoesNotExist($this->themePath . '/node_modules');
$this->assertFileNotExists($this->lockPath);
$this->withPackageJsonRestore(function () {
// Run npm install for a single dev package
$this->artisan('npm:install', [
'package' => 'theme-npmtest',
'npmArgs' => ['is-odd'],
'--dev' => true,
'--disable-tty' => true
])
->assertExitCode(0);
// Validate lock file was generated successfully
$this->assertFileExists($this->lockPath);
// Validate the contents of package.json
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayHasKey('devDependencies', $packageJson);
$this->assertArrayHasKey('is-odd', $packageJson['devDependencies']);
// Validate that node_modules paths exist
$this->assertDirectoryExists($this->themePath . '/node_modules');
$this->assertDirectoryExists($this->themePath . '/node_modules/is-odd');
});
}
/**
* Test the ability to install multiple dev npm packages via artisan
*/
public function testNpmInstallMultipleDev(): void
{
// Validate the package Json does not have dependencies
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayNotHasKey('devDependencies', $packageJson);
// Validate node_modules not found
$this->assertDirectoryDoesNotExist($this->themePath . '/node_modules');
$this->assertFileNotExists($this->lockPath);
$this->withPackageJsonRestore(function () {
// Run npm install for multiple dev packages
$this->artisan('npm:install', [
'package' => 'theme-npmtest',
'npmArgs' => ['is-odd', 'is-even'],
'--dev' => true,
'--disable-tty' => true
])
->assertExitCode(0);
// Validate lock file was generated successfully
$this->assertFileExists($this->lockPath);
// Validate the contents of package.json
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayHasKey('devDependencies', $packageJson);
$this->assertArrayHasKey('is-odd', $packageJson['devDependencies']);
$this->assertArrayHasKey('is-even', $packageJson['devDependencies']);
// Validate that node_modules paths exist
$this->assertDirectoryExists($this->themePath . '/node_modules');
$this->assertDirectoryExists($this->themePath . '/node_modules/is-odd');
$this->assertDirectoryExists($this->themePath . '/node_modules/is-even');
});
}
/**
* Cleanup the test theme
*/
public function tearDown(): void
{
if (File::isDirectory($this->themePath . '/node_modules')) {
File::deleteDirectory($this->themePath . '/node_modules');
}
foreach ([$this->backupPath, $this->lockPath] as $path) {
if (File::exists($path)) {
File::delete($path);
}
}
parent::tearDown();
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace System\Tests\Console\Asset\Npm;
use System\Classes\Asset\PackageManager;
use System\Tests\Bootstrap\TestCase;
use Winter\Storm\Support\Facades\File;
class NpmRunTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
if (!File::exists(base_path('node_modules'))) {
$this->markTestSkipped('This test requires node_modules to be installed');
}
// Add our testing theme because it won't be auto discovered
PackageManager::instance()->registerPackage(
'theme-npmtest',
base_path('modules/system/tests/fixtures/themes/npmtest/vite.config.mjs'),
'vite'
);
}
/**
* Test the ability to run a npm script via artisan
*/
public function testNpmRunScript(): void
{
$this->artisan('npm:run', [
'package' => 'theme-npmtest',
'script' => 'testScript',
'--disable-tty' => true
])
->expectsOutputToContain('> echo "Winter says $((1+2))"')
->expectsOutputToContain('Winter says 3')
->assertExitCode(0);
}
/**
* Test the error handling of a missing script
*/
public function testNpmRunScriptFailed(): void
{
$this->artisan('npm:run', [
'package' => 'theme-npmtest',
'script' => 'testMissingScript',
'--disable-tty' => true
])
->expectsOutputToContain('Script "testMissingScript" is not defined in package "theme-npmtest"')
->assertExitCode(1);
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace System\tests\console\asset\npm;
use System\Classes\Asset\PackageManager;
use System\Tests\Bootstrap\TestCase;
use System\Tests\Console\Asset\NpmTestTrait;
use Winter\Storm\Support\Facades\File;
class NpmUpdateTest extends TestCase
{
use NpmTestTrait;
protected string $themePath;
protected string $jsonPath;
protected string $lockPath;
protected string $backupPath;
public function setUp(): void
{
parent::setUp();
if (!File::exists(base_path('node_modules'))) {
$this->markTestSkipped('This test requires node_modules to be installed');
}
// Define some helpful paths
$this->themePath = base_path('modules/system/tests/fixtures/themes/npmtest');
$this->jsonPath = $this->themePath . '/package.json';
$this->lockPath = $this->themePath . '/package-lock.json';
$this->backupPath = $this->themePath . '/package.backup.json';
// Add our testing theme because it won't be auto discovered
PackageManager::instance()->registerPackage(
'theme-npmtest',
$this->themePath . '/vite.config.mjs',
'vite'
);
}
/**
* Test the ability to install a single npm package via artisan
*/
public function testNpmUpdate(): void
{
// Validate the package Json does not have dependencies
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayNotHasKey('dependencies', $packageJson);
// Validate node_modules not found
$this->assertDirectoryDoesNotExist($this->themePath . '/node_modules');
$this->assertFileNotExists($this->lockPath);
$this->withPackageJsonRestore(function () {
// Update the contents of package.json to include a package at an old patch
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$packageJson['dependencies'] = [
'is-odd' => '^3.0.0'
];
File::put($this->jsonPath, json_encode($packageJson, JSON_PRETTY_PRINT));
// Run npm update
$this->artisan('npm:update', [
'package' => 'theme-npmtest',
'--save' => true,
'--disable-tty' => true
])
->assertExitCode(0);
// Validate lock file was generated successfully
$this->assertFileExists($this->lockPath);
// Get the new contents of package.json
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
// Validate that the package.json contents does not match the old patch value we added above
$this->assertArrayHasKey('dependencies', $packageJson);
$this->assertArrayHasKey('is-odd', $packageJson['dependencies']);
$this->assertNotEquals('^3.0.0', $packageJson['dependencies']['is-odd']);
// Validate that node_modules paths exist
$this->assertDirectoryExists($this->themePath . '/node_modules');
$this->assertDirectoryExists($this->themePath . '/node_modules/is-odd');
});
}
/**
* Cleanup the test theme
*/
public function tearDown(): void
{
if (File::isDirectory($this->themePath . '/node_modules')) {
File::deleteDirectory($this->themePath . '/node_modules');
}
foreach ([$this->backupPath, $this->lockPath] as $path) {
if (File::exists($path)) {
File::delete($path);
}
}
parent::tearDown();
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace System\Tests\Console\Asset\Vite;
use System\Classes\Asset\PackageManager;
use System\Tests\Bootstrap\TestCase;
use Winter\Storm\Support\Facades\File;
class ViteCompileTest extends TestCase
{
protected string $themePath;
public function setUp(): void
{
parent::setUp();
if (!File::exists(base_path('node_modules'))) {
$this->markTestSkipped('This test requires node_modules to be installed');
}
if (!File::exists(base_path('node_modules/.bin/vite'))) {
$this->markTestSkipped('This test requires the vite package to be installed');
}
$this->themePath = base_path('modules/system/tests/fixtures/themes/assettest');
// Add our testing theme because it won't be auto discovered
PackageManager::instance()->registerPackage(
'theme-assettest',
$this->themePath . '/vite.config.mjs',
'vite'
);
}
public function testThemeCompile(): void
{
// Run the vite:compile command
$this->artisan('vite:compile', [
'theme-assettest',
'--manifest' => 'modules/system/tests/fixtures/npm/package-vitetheme.json',
'--silent' => true
])->assertExitCode(0);
$manifestPath = $this->themePath . '/public/build/manifest.json';
// Validate the manifest was written
$this->assertFileExists($manifestPath);
// Get the contents of the manifest
$manifest = json_decode(File::get($manifestPath), JSON_OBJECT_AS_ARRAY);
// Validate the css was compiled correctly
$this->assertArrayHasKey('assets/css/theme.css', $manifest);
$this->assertFileExists($this->themePath . '/public/build/' . $manifest['assets/css/theme.css']['file']);
$this->assertEquals(
'h1{color:red}',
trim(File::get($this->themePath . '/public/build/' . $manifest['assets/css/theme.css']['file']))
);
// Validate the js was compiled correctly
$this->assertArrayHasKey('assets/javascript/theme.js', $manifest);
$this->assertFileExists($this->themePath . '/public/build/' . $manifest['assets/javascript/theme.js']['file']);
$this->assertEquals(
'window.alert("hello world");',
trim(File::get($this->themePath . '/public/build/' . $manifest['assets/javascript/theme.js']['file']))
);
}
public function testThemeCompileFailed(): void
{
// Rename the css file so vite cannot find it
File::move($this->themePath . '/assets/css/theme.css', $this->themePath . '/assets/css/theme.back');
// Run the vite:compile command
$this->artisan('vite:compile', [
'theme-assettest',
'--manifest' => 'modules/system/tests/fixtures/npm/package-vitetheme.json',
'--disable-tty' => true
])
->expectsOutputToContain('Could not resolve entry module "assets/css/theme.css".')
->assertExitCode(1);
// Validate the manifest was not written
$this->assertFileNotExists($this->themePath . '/public/build/manifest.json');
// Put the css file back
File::move($this->themePath . '/assets/css/theme.back', $this->themePath . '/assets/css/theme.css');
}
public function testThemeCompileWarning(): void
{
// Rename the css file so vite cannot find it
$contents = File::get($this->themePath . '/assets/css/theme.css');
File::put($this->themePath . '/assets/css/theme.css', 'h1 {color:');
// Run the vite:compile command
$this->artisan('vite:compile', [
'theme-assettest',
'--manifest' => 'modules/system/tests/fixtures/npm/package-vitetheme.json',
'--disable-tty' => true
])
->expectsOutputToContain('[WARNING] Expected "}" to go with "{" [css-syntax-error]')
->assertExitCode(0);
// Validate the manifest was not written
$this->assertFileExists($this->themePath . '/public/build/manifest.json');
// Put the css file back
File::put($this->themePath . '/assets/css/theme.css', $contents);
}
public function tearDown(): void
{
File::deleteDirectory('modules/system/tests/fixtures/themes/assettest/public');
parent::tearDown();
}
}

View File

@@ -0,0 +1,234 @@
<?php
namespace System\Tests\Console\Asset\Vite;
use System\Classes\PluginManager;
use System\Tests\Bootstrap\TestCase;
use Winter\Storm\Support\Facades\File;
class ViteCreateTest extends TestCase
{
protected string $testPlugin = 'Winter.Sample';
public function testConfigWritten(): void
{
$path = PluginManager::instance()->findByIdentifier($this->testPlugin)->getPluginPath();
$configPath = $path . '/vite.config.mjs';
// Check file does not exist
$this->assertFileNotExists($configPath);
// Run the config command to generate the vite config
$this->artisan('vite:create', [
'packageName' => $this->testPlugin,
'--no-stubs' => true,
])
->doesntExpectOutput('vite.config.mjs already exists, overwrite?')
->assertExitCode(0);
// Validate the manifest was written
$this->assertFileExists($configPath);
// Get the predicted contents
$fixture = str_replace(
'{{packageName}}',
'winter-sample',
File::get(base_path('modules/system/console/asset/fixtures/config/vite/vite.config.mjs.fixture')),
);
// Check the file written is what was expected
$this->assertEquals($fixture, File::get($configPath));
// Overwrite the file content
File::put($configPath, 'testing');
// Check that refusing to overwrite does not replace file contents
$this->artisan('vite:create', [
'packageName' => $this->testPlugin,
'--no-stubs' => true,
])
->expectsQuestion('vite.config.mjs already exists, overwrite?', false)
->assertExitCode(0);
// Check file contents was not overwritten
$this->assertNotEquals($fixture, File::get($configPath));
// Run command confirming to overwrite file contents works
$this->artisan('vite:create', [
'packageName' => $this->testPlugin,
'--no-stubs' => true,
])
->expectsQuestion('vite.config.mjs already exists, overwrite?', true)
->assertExitCode(0);
// Check file contents was overwritten
$this->assertEquals($fixture, File::get($configPath));
}
public function testConfigTailwind(): void
{
$path = PluginManager::instance()->findByIdentifier($this->testPlugin)->getPluginPath();
$configPath = $path . '/vite.config.mjs';
$packageJson = $path . '/package.json';
// Check file does not exist
$this->assertFileNotExists($configPath);
// Run the config command to generate the vite config
$this->artisan('vite:create', [
'packageName' => $this->testPlugin,
'--tailwind' => true,
'--no-stubs' => true,
])
->assertExitCode(0);
// Check files are created correctly
$this->assertFileExists($configPath);
$this->assertFileExists($path . '/tailwind.config.js');
$this->assertFileExists($path . '/postcss.config.mjs');
// Get the contents of the package.json
$json = json_decode(File::get($packageJson));
// Check tailwindcss is required
$this->assertTrue(isset($json->devDependencies->tailwindcss));
}
public function testConfigReact(): void
{
$path = PluginManager::instance()->findByIdentifier($this->testPlugin)->getPluginPath();
$configPath = $path . '/vite.config.mjs';
$packageJson = $path . '/package.json';
// Check file does not exist
$this->assertFileNotExists($configPath);
// Run the config command to generate the vite config with react
$this->artisan('vite:create', [
'packageName' => $this->testPlugin,
'--react' => true,
'--no-stubs' => true,
])
->assertExitCode(0);
// Check files are created correctly
$this->assertFileExists($configPath);
$this->assertFileExists($packageJson);
// Get the contents of the package.json
$json = json_decode(File::get($packageJson));
// Check react is required
$this->assertTrue(isset($json->devDependencies->react));
}
public function testConfigTailwindReact(): void
{
$path = PluginManager::instance()->findByIdentifier($this->testPlugin)->getPluginPath();
$configPath = $path . '/vite.config.mjs';
$packageJson = $path . '/package.json';
// Check file does not exist
$this->assertFileNotExists($configPath);
// Run the config command to generate the vite config with react
$this->artisan('vite:create', [
'packageName' => $this->testPlugin,
'--tailwind' => true,
'--react' => true,
'--no-stubs' => true,
])
->assertExitCode(0);
// Check files are created correctly
$this->assertFileExists($configPath);
$this->assertFileExists($packageJson);
$this->assertFileExists($path . '/tailwind.config.js');
$this->assertFileExists($path . '/postcss.config.mjs');
// Get the contents of the package.json
$json = json_decode(File::get($packageJson));
// Check tailwind & react are required
$this->assertTrue(isset($json->devDependencies->tailwindcss));
$this->assertTrue(isset($json->devDependencies->react));
}
public function testConfigVue(): void
{
$path = PluginManager::instance()->findByIdentifier($this->testPlugin)->getPluginPath();
$configPath = $path . '/vite.config.mjs';
$packageJson = $path . '/package.json';
// Check file does not exist
$this->assertFileNotExists($configPath);
// Run the config command to generate the vite config with vue
$this->artisan('vite:create', [
'packageName' => $this->testPlugin,
'--vue' => true,
'--no-stubs' => true,
])
->assertExitCode(0);
// Check files are created correctly
$this->assertFileExists($configPath);
$this->assertFileExists($packageJson);
// Get the contents of the package.json
$json = json_decode(File::get($packageJson));
// Check vue is required
$this->assertTrue(isset($json->devDependencies->vue));
}
public function testConfigTailwindVue(): void
{
$path = PluginManager::instance()->findByIdentifier($this->testPlugin)->getPluginPath();
$configPath = $path . '/vite.config.mjs';
$packageJson = $path . '/package.json';
// Check file does not exist
$this->assertFileNotExists($configPath);
// Run the config command to generate the vite config with vue
$this->artisan('vite:create', [
'packageName' => $this->testPlugin,
'--tailwind' => true,
'--vue' => true,
'--no-stubs' => true,
])
->assertExitCode(0);
// Check files are created correctly
$this->assertFileExists($configPath);
$this->assertFileExists($packageJson);
$this->assertFileExists($path . '/tailwind.config.js');
$this->assertFileExists($path . '/postcss.config.mjs');
// Get the contents of the package.json
$json = json_decode(File::get($packageJson));
// Check tailwind & vue are required
$this->assertTrue(isset($json->devDependencies->tailwindcss));
$this->assertTrue(isset($json->devDependencies->vue));
}
public function tearDown(): void
{
$path = PluginManager::instance()->findByIdentifier($this->testPlugin)->getPluginPath();
$files = [
$path . '/vite.config.mjs',
$path . '/package.json',
$path . '/tailwind.config.js',
$path . '/postcss.config.mjs',
];
foreach ($files as $file) {
if (File::exists($file)) {
File::delete($file);
}
}
}
}

View File

@@ -0,0 +1,223 @@
<?php
namespace System\Tests\Console\Asset\Vite;
use System\Classes\Asset\PackageManager;
use System\Tests\Bootstrap\TestCase;
use Winter\Storm\Exception\SystemException;
use Winter\Storm\Support\Facades\File;
class ViteInstallTest extends TestCase
{
protected string $jsonPath;
protected string $lockPath;
protected string $backupPath;
public function setUp(): void
{
parent::setUp();
if (!File::exists(base_path('node_modules'))) {
$this->markTestSkipped('This test requires node_modules to be installed');
}
// Define some helpful paths
$this->fixturePath = base_path('modules/system/tests');
$this->jsonPath = $this->fixturePath . '/package.json';
$this->lockPath = $this->fixturePath . '/package-lock.json';
$this->backupPath = $this->fixturePath . '/package-testing.json';
// Add our testing theme because it won't be auto discovered
PackageManager::instance()->registerPackage(
'theme-assettest',
base_path('modules/system/tests/fixtures/themes/assettest/vite.config.mjs'),
'vite'
);
PackageManager::instance()->registerPackage(
'theme-npmtest',
base_path('modules/system/tests/fixtures/themes/npmtest/vite.config.mjs'),
'vite'
);
}
public function testViteInstallMissingPackageJson(): void
{
$this->artisan('vite:install', [
'assetPackage' => ['theme-assettest'],
'--package-json' => '/some/file',
'--no-install' => true
])
->expectsOutputToContain('The supplied --package-json path does not exist.')
->assertExitCode(1);
}
public function testViteInstallSinglePackage(): void
{
$this->withPackageJsonRestore(function () {
$this->artisan('vite:install', [
'assetPackage' => ['theme-assettest'],
'--package-json' => $this->jsonPath,
'--no-install' => true
])
->expectsQuestion('vite was not found as a dependency in package.json, would you like to add it?', true)
->expectsQuestion('laravel-vite-plugin was not found as a dependency in package.json, would you like to add it?', true)
->expectsOutput('Adding theme-assettest (modules/system/tests/fixtures/themes/assettest) to the workspaces.packages property in package.json')
->assertExitCode(0);
$this->assertFileExists($this->jsonPath);
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayHasKey('devDependencies', $packageJson);
$this->assertArrayHasKey('vite', $packageJson['devDependencies']);
$this->assertArrayHasKey('laravel-vite-plugin', $packageJson['devDependencies']);
$this->assertArrayHasKey('workspaces', $packageJson);
$this->assertArrayHasKey('packages', $packageJson['workspaces']);
$this->assertContains('modules/system/tests/fixtures/themes/assettest', $packageJson['workspaces']['packages']);
});
}
public function testViteInstallMultiplePackages(): void
{
$this->withPackageJsonRestore(function () {
$this->artisan('vite:install', [
'assetPackage' => ['theme-assettest', 'theme-npmtest'],
'--package-json' => $this->jsonPath,
'--no-install' => true
])
->expectsQuestion('vite was not found as a dependency in package.json, would you like to add it?', true)
->expectsQuestion('laravel-vite-plugin was not found as a dependency in package.json, would you like to add it?', true)
->expectsOutput('Adding theme-assettest (modules/system/tests/fixtures/themes/assettest) to the workspaces.packages property in package.json')
->expectsOutput('Adding theme-npmtest (modules/system/tests/fixtures/themes/npmtest) to the workspaces.packages property in package.json')
->assertExitCode(0);
$this->assertFileExists($this->jsonPath);
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayHasKey('devDependencies', $packageJson);
$this->assertArrayHasKey('vite', $packageJson['devDependencies']);
$this->assertArrayHasKey('laravel-vite-plugin', $packageJson['devDependencies']);
$this->assertArrayHasKey('workspaces', $packageJson);
$this->assertArrayHasKey('packages', $packageJson['workspaces']);
$this->assertContains('modules/system/tests/fixtures/themes/assettest', $packageJson['workspaces']['packages']);
$this->assertContains('modules/system/tests/fixtures/themes/npmtest', $packageJson['workspaces']['packages']);
});
}
public function testViteInstallMissingPackage(): void
{
// We should receive an exception for a missing package
$this->withPackageJsonRestore(function () {
$this->expectException(SystemException::class);
$this->expectExceptionMessage('PackageNotFoundException: The package `theme-assettest2` does not exist.');
$this->artisan('vite:install', [
'assetPackage' => ['theme-assettest2'],
'--package-json' => $this->jsonPath,
'--no-install' => true
])
->assertExitCode(1);
});
}
public function testViteInstallRelativePath(): void
{
$this->withPackageJsonRestore(function () {
$this->artisan('vite:install', [
'assetPackage' => ['theme-assettest'],
'--package-json' => 'modules/system/tests/package.json',
'--no-install' => true
])
->expectsQuestion('vite was not found as a dependency in package.json, would you like to add it?', true)
->expectsQuestion('laravel-vite-plugin was not found as a dependency in package.json, would you like to add it?', true)
->expectsOutput('Adding theme-assettest (modules/system/tests/fixtures/themes/assettest) to the workspaces.packages property in package.json')
->assertExitCode(0);
});
}
public function testViteInstallIgnoredPackage(): void
{
$this->withPackageJsonRestore(function () {
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$packageJson['workspaces'] = [
'ignoredPackages' => [
'modules/system/tests/fixtures/themes/assettest'
]
];
File::put($this->jsonPath, json_encode($packageJson, JSON_PRETTY_PRINT));
$this->artisan('vite:install', [
'assetPackage' => ['theme-assettest'],
'--package-json' => $this->jsonPath,
'--no-install' => true
])
->expectsQuestion('vite was not found as a dependency in package.json, would you like to add it?', false)
->expectsQuestion('laravel-vite-plugin was not found as a dependency in package.json, would you like to add it?', false)
->expectsOutput('The requested package theme-assettest (modules/system/tests/fixtures/themes/assettest) is ignored, remove it from package.json to continue.')
->assertExitCode(0);
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayHasKey('workspaces', $packageJson);
$this->assertArrayNotHasKey('packages', $packageJson['workspaces']);
$this->assertArrayNotHasKey('dependencies', $packageJson);
$this->assertArrayNotHasKey('devDependencies', $packageJson);
});
}
public function testViteInstallWithNpmInstall(): void
{
$this->withPackageJsonRestore(function () {
$this->assertDirectoryDoesNotExist($this->fixturePath . '/node_modules');
$this->artisan('vite:install', [
'assetPackage' => ['theme-assettest'],
'--package-json' => $this->jsonPath,
'--disable-tty' => true
])
->expectsQuestion('vite was not found as a dependency in package.json, would you like to add it?', true)
->expectsQuestion('laravel-vite-plugin was not found as a dependency in package.json, would you like to add it?', true)
->expectsOutput('Adding theme-assettest (modules/system/tests/fixtures/themes/assettest) to the workspaces.packages property in package.json')
->expectsOutputToContain('packages, and audited') // output from npm i
->assertExitCode(0);
$this->assertFileExists($this->jsonPath);
$packageJson = json_decode(File::get($this->jsonPath), JSON_OBJECT_AS_ARRAY);
$this->assertArrayHasKey('devDependencies', $packageJson);
$this->assertArrayHasKey('vite', $packageJson['devDependencies']);
$this->assertArrayHasKey('laravel-vite-plugin', $packageJson['devDependencies']);
$this->assertArrayHasKey('workspaces', $packageJson);
$this->assertArrayHasKey('packages', $packageJson['workspaces']);
$this->assertContains('modules/system/tests/fixtures/themes/assettest', $packageJson['workspaces']['packages']);
$this->assertFileExists($this->lockPath);
$this->assertDirectoryExists($this->fixturePath . '/node_modules');
$this->assertDirectoryExists($this->fixturePath . '/node_modules/vite');
$this->assertDirectoryExists($this->fixturePath . '/node_modules/laravel-vite-plugin');
});
}
/**
* Helper to run test logic and handle restoring package.json file after
*/
protected function withPackageJsonRestore(callable $callback): void
{
File::copy($this->backupPath, $this->jsonPath);
$callback();
File::delete($this->jsonPath);
}
public function tearDown(): void
{
if (File::isDirectory($this->fixturePath . '/node_modules')) {
File::deleteDirectory($this->fixturePath . '/node_modules');
}
if (File::exists($this->lockPath)) {
File::delete($this->lockPath);
}
parent::tearDown();
}
}

View File

@@ -0,0 +1,9 @@
<?php
// Fixture used for `winter:env` unit tests in `tests/unit/system/console/WinterEnvTest.php
return [
'debug' => true,
'url' => 'https://env-test.localhost',
'key' => 'CHANGE_ME!!!!!!!!!!!!!!!!!!!!!!!',
'timezone' => 'UTC',
];

View File

@@ -0,0 +1,6 @@
<?php
// Fixture used for `winter:env` unit tests in `tests/unit/system/console/WinterEnvTest.php
return [
'default' => 'file',
];

View File

@@ -0,0 +1,10 @@
<?php
// Fixture used for `winter:env` unit tests in `tests/unit/system/console/WinterEnvTest.php
return [
'enableRoutesCache' => false,
'enableAssetCache' => false,
'databaseTemplates' => false,
'linkPolicy' => 'detect',
'enableCsrfProtection' => true,
];

View File

@@ -0,0 +1,16 @@
<?php
// Fixture used for `winter:env` unit tests in `tests/unit/system/console/WinterEnvTest.php
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
'host' => 'localhost',
'port' => 3306,
'database' => 'data#base',
'username' => 'teal\'c',
'password' => 'test"quotes\'test',
],
],
'useConfigForTesting' => false,
];

View File

@@ -0,0 +1,11 @@
<?php
// Fixture used for `winter:env` unit tests in `tests/unit/system/console/WinterEnvTest.php
return [
'default' => 'smtp',
'host' => 'smtp.mailgun.org',
'port' => 587,
'encryption' => 'tls',
'username' => null,
'password' => null,
];

View File

@@ -0,0 +1,6 @@
<?php
// Fixture used for `winter:env` unit tests in `tests/unit/system/console/WinterEnvTest.php
return [
'default' => 'sync',
];

View File

@@ -0,0 +1,6 @@
<?php
// Fixture used for `winter:env` unit tests in `tests/unit/system/console/WinterEnvTest.php
return [
'driver' => 'file',
];

View File

@@ -0,0 +1,2 @@
<?php
// file1.php - version 1.

View File

@@ -0,0 +1,2 @@
<?php
// file1.php - version 1.

View File

@@ -0,0 +1,2 @@
<?php
// file2.php - version 1.

View File

@@ -0,0 +1,2 @@
<?php
// file1.php - version 1 / test2.

View File

@@ -0,0 +1,2 @@
<?php
// file2.php - version 2.

View File

@@ -0,0 +1,2 @@
<?php
// file3.php - version 1.

View File

@@ -0,0 +1,2 @@
<?php
// file1.php - version 1 / test2.

View File

@@ -0,0 +1,2 @@
<?php
// file2.php - version 2.

View File

@@ -0,0 +1,2 @@
<?php
// file1.php - version 3 / test2.

View File

@@ -0,0 +1,2 @@
<?php
// file3.php - version 2.

View File

@@ -0,0 +1,2 @@
<?php
// file4.php - version 1.

View File

@@ -0,0 +1,2 @@
<?php
// file1.php - version 2 / test2.

View File

@@ -0,0 +1,2 @@
<?php
// file1.php - version 1 / test3.

View File

@@ -0,0 +1,2 @@
<?php
// file3.php - version 2.

View File

@@ -0,0 +1,2 @@
<?php
// file4.php - version 1.

View File

@@ -0,0 +1,2 @@
<?php
// file1.php - version 3 / test2.

View File

@@ -0,0 +1,2 @@
<?php
// file1.php - version 2 / test3.

View File

@@ -0,0 +1,2 @@
<?php
// file2.php - version 1 / test3.

View File

@@ -0,0 +1,5 @@
{
"forks": {
"1.1.0": "1.0.2"
}
}

View File

@@ -0,0 +1 @@
THIS IS A TEXT DOCUMENT

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1,12 @@
{
"devDependencies": {
"laravel-mix": "^6.0.41"
},
"workspaces": {
"packages": [
"modules/system/tests/fixtures/plugins/mix/testa",
"modules/system/tests/fixtures/plugins/mix/testb",
"modules/system/tests/fixtures/plugins/mix/testc"
]
}
}

View File

@@ -0,0 +1,11 @@
{
"devDependencies": {
"laravel-mix": "^6.0.41"
},
"workspaces": {
"packages": [
"modules/system/tests/fixtures/plugins/mix/testa",
"modules/system/tests/fixtures/plugins/mix/testc"
]
}
}

View File

@@ -0,0 +1,8 @@
{
"workspaces": {
"packages": [
"plugins/winter/demo",
"themes/demo"
]
},
"dependencies":

View File

@@ -0,0 +1,24 @@
{
"workspaces": {
"packages": [
"plugins/winter/demo",
"themes/demo"
],
"ignoredPackages": [
"modules/backend",
"modules/system"
]
},
"dependencies": {
"example": "^2.0.1",
"test": "^1.0.0"
},
"devDependencies": {
"test-dev": "^3.0.2"
},
"scripts": {
"test": "testing",
"example": "example test",
"foo": "bar ./test"
}
}

View File

@@ -0,0 +1,12 @@
{
"type": "module",
"workspaces": {
"packages": [
"modules/system/tests/fixtures/themes/assettest"
]
},
"devDependencies": {
"laravel-vite-plugin": "^1.0.2",
"vite": "^5.2.11"
}
}

View File

@@ -0,0 +1,15 @@
<?php namespace Database\Tester;
use System\Classes\PluginBase;
class Plugin extends PluginBase
{
public function pluginDetails()
{
return [
'name' => 'Database Tester Plugin',
'description' => 'Plugin for loading tests that involve the database.',
'author' => 'Alexey Bobkov, Samuel Georges'
];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,68 @@
<?php namespace Database\Tester\Models;
use Model;
class Author extends Model
{
/**
* @var string The database table used by the model.
*/
public $table = 'database_tester_authors';
/**
* @var array Guarded fields
*/
protected $guarded = [];
/**
* @var array Relations
*/
public $belongsTo = [
'user' => ['Database\Tester\Models\User', 'delete' => true],
'country' => ['Database\Tester\Models\Country'],
'user_soft' => ['Database\Tester\Models\SoftDeleteUser', 'key' => 'user_id', 'softDelete' => true],
];
public $hasMany = [
'posts' => 'Database\Tester\Models\Post',
];
public $hasOne = [
'phone' => 'Database\Tester\Models\Phone',
];
public $belongsToMany = [
'roles' => [
'Database\Tester\Models\Role',
'table' => 'database_tester_authors_roles'
],
'executive_authors' => [
'Database\Tester\Models\Role',
'table' => 'database_tester_authors_roles',
'conditions' => 'is_executive = 1'
],
];
public $morphMany = [
'event_log' => ['Database\Tester\Models\EventLog', 'name' => 'related', 'delete' => true, 'softDelete' => true],
];
public $morphOne = [
'meta' => ['Database\Tester\Models\Meta', 'name' => 'taggable'],
];
public $morphToMany = [
'tags' => [
'Database\Tester\Models\Tag',
'name' => 'taggable',
'table' => 'database_tester_taggables',
'pivot' => ['added_by']
],
];
}
class SoftDeleteAuthor extends Author
{
use \Winter\Storm\Database\Traits\SoftDelete;
}

View File

@@ -0,0 +1,40 @@
<?php namespace Database\Tester\Models;
use Model;
class Category extends Model
{
/**
* @var string The database table used by the model.
*/
public $table = 'database_tester_categories';
public $belongsToMany = [
'posts' => [
'Database\Tester\Models\Post',
'table' => 'database_tester_categories_posts',
'pivot' => ['category_name', 'post_name']
]
];
public function getCustomNameAttribute()
{
return $this->name.' (#'.$this->id.')';
}
}
class CategorySimple extends Category
{
use \Winter\Storm\Database\Traits\SimpleTree;
}
class CategoryNested extends Category
{
use \Winter\Storm\Database\Traits\NestedTree;
/**
* @var string The database table used by the model.
*/
public $table = 'database_tester_categories_nested';
}

View File

@@ -0,0 +1,35 @@
<?php namespace Database\Tester\Models;
use Model;
class Country extends Model
{
/**
* @var string The database table used by the model.
*/
public $table = 'database_tester_countries';
/**
* @var array Guarded fields
*/
protected $guarded = [];
public $hasMany = [
'users' => [
'Database\Tester\Models\User',
],
];
public $hasManyThrough = [
'posts' => [
'Database\Tester\Models\Post',
'through' => 'Database\Tester\Models\Author',
]
];
}
class SoftDeleteCountry extends Country
{
use \Winter\Storm\Database\Traits\SoftDelete;
}

View File

@@ -0,0 +1,30 @@
<?php namespace Database\Tester\Models;
use Model;
class EventLog extends Model
{
use \Winter\Storm\Database\Traits\SoftDelete;
/**
* @var string The database table used by the model.
*/
public $table = 'database_tester_event_log';
/**
* @var array Guarded fields
*/
protected $guarded = ['*'];
/**
* @var array Fillable fields
*/
protected $fillable = [];
/**
* @var array Relations
*/
public $morphTo = [
'related' => []
];
}

View File

@@ -0,0 +1,24 @@
<?php namespace Database\Tester\Models;
use Model;
class Meta extends Model
{
public $table = 'database_tester_meta';
public $timestamps = false;
public $morphTo = [
'taggable' => []
];
public $fillable = [
'meta_title',
'meta_description',
'meta_keywords',
'canonical_url',
'redirect_url',
'robot_index',
'robot_follow'
];
}

View File

@@ -0,0 +1,29 @@
<?php namespace Database\Tester\Models;
use Model;
class Phone extends Model
{
/**
* @var string The database table used by the model.
*/
public $table = 'database_tester_phones';
/**
* @var array Guarded fields
*/
protected $guarded = ['*'];
/**
* @var array Fillable fields
*/
protected $fillable = [];
/**
* @var array Relations
*/
public $belongsTo = [
'author' => 'Database\Tester\Models\Author',
];
}

View File

@@ -0,0 +1,151 @@
<?php namespace Database\Tester\Models;
use Model;
class Post extends Model
{
/**
* @var string The database table used by the model.
*/
public $table = 'database_tester_posts';
/**
* @var array Guarded fields
*/
protected $guarded = ['*'];
/**
* @var array Fillable fields
*/
protected $fillable = [];
/**
* @var array Relations
*/
public $belongsTo = [
'author' => 'Database\Tester\Models\Author',
];
public $belongsToMany = [
'categories' => [
'Database\Tester\Models\Category',
'table' => 'database_tester_categories_posts',
'pivot' => ['category_name', 'post_name']
]
];
public $morphMany = [
'event_log' => ['Database\Tester\Models\EventLog', 'name' => 'related', 'delete' => true, 'softDelete' => true],
];
public $morphOne = [
'meta' => ['Database\Tester\Models\Meta', 'name' => 'taggable'],
];
public $morphToMany = [
'tags' => [
'Database\Tester\Models\Tag',
'name' => 'taggable',
'table' => 'database_tester_taggables',
'pivot' => ['added_by']
],
];
}
class NullablePost extends Post
{
use \Winter\Storm\Database\Traits\Nullable;
/**
* @var array Guarded fields
*/
protected $guarded = [];
/**
* @var array List of attributes to nullify
*/
protected $nullable = [
'author_nickname',
];
}
class SluggablePost extends Post
{
use \Winter\Storm\Database\Traits\Sluggable;
/**
* @var array Guarded fields
*/
protected $guarded = [];
/**
* @var array List of attributes to automatically generate unique URL names (slugs) for.
*/
protected $slugs = [
'slug' => 'title',
'long_slug' => ['title', 'description']
];
}
class RevisionablePost extends Post
{
use \Winter\Storm\Database\Traits\Revisionable;
use \Winter\Storm\Database\Traits\SoftDelete;
/**
* @var array Guarded fields
*/
protected $guarded = [];
/**
* @var array Dates
*/
protected $dates = ['published_at', 'deleted_at'];
/**
* @var array Monitor these attributes for changes.
*/
protected $revisionable = [
'title',
'slug',
'description',
'is_published',
'published_at',
'deleted_at'
];
/**
* @var int Maximum number of revision records to keep.
*/
public $revisionableLimit = 8;
/**
* @var array Relations
*/
public $morphMany = [
'revision_history' => ['System\Models\Revision', 'name' => 'revisionable']
];
/**
* The user who made the revision.
*/
public function getRevisionableUser()
{
return 7;
}
}
class ValidationPost extends Post
{
use \Winter\Storm\Database\Traits\Validation;
/**
* @var array Guarded fields
*/
protected $guarded = [];
public $rules = [
'title' => 'required|min:3|max:255',
'slug' => ['required', 'regex:/^[a-z0-9\/\:_\-\*\[\]\+\?\|]*$/i', 'unique:database_tester_posts'],
];
}

View File

@@ -0,0 +1,34 @@
<?php namespace Database\Tester\Models;
use Model;
/**
* Role Model
*/
class Role extends Model
{
/**
* @var string The database table used by the model.
*/
public $table = 'database_tester_roles';
/**
* @var array Guarded fields
*/
protected $guarded = [];
/**
* @var array Fillable fields
*/
protected $fillable = [];
/**
* @var array Relations
*/
public $belongsToMany = [
'authors' => [
'Database\Tester\Models\User',
'table' => 'database_tester_authors_roles'
],
];
}

View File

@@ -0,0 +1,36 @@
<?php namespace Database\Tester\Models;
use Model;
class Tag extends Model
{
/**
* @var string The database table used by the model.
*/
public $table = 'database_tester_tags';
/**
* @var array Guarded fields
*/
protected $guarded = [];
/**
* @var array Fillable fields
*/
protected $fillable = [];
public $morphedByMany = [
'authors' => [
'Database\Tester\Models\Author',
'name' => 'taggable',
'table' => 'database_tester_taggables',
'pivot' => ['added_by'],
],
'posts' => [
'Database\Tester\Models\Post',
'name' => 'taggable',
'table' => 'database_tester_taggables',
'pivot' => ['added_by'],
],
];
}

View File

@@ -0,0 +1,69 @@
<?php namespace Database\Tester\Models;
use Model;
class User extends Model
{
/**
* @var string The database table used by the model.
*/
public $table = 'database_tester_users';
/**
* @var array Guarded fields
*/
protected $guarded = [];
/**
* @var array Relations
*/
public $hasOne = [
'author' => [
'Database\Tester\Models\Author',
]
];
public $hasOneThrough = [
'phone' => [
'Database\Tester\Models\Phone',
'through' => 'Database\Tester\Models\Author',
],
];
public $attachOne = [
'avatar' => 'System\Models\File'
];
public $attachMany = [
'photos' => 'System\Models\File'
];
}
class SoftDeleteUser extends User
{
use \Winter\Storm\Database\Traits\SoftDelete;
}
class UserWithAuthor extends User
{
public $hasOne = [
'author' => ['Database\Tester\Models\Author', 'key' => 'user_id', 'delete' => true],
];
}
class UserWithSoftAuthor extends User
{
public $hasOne = [
'author' => ['Database\Tester\Models\SoftDeleteAuthor', 'key' => 'user_id', 'softDelete' => true],
];
}
class UserWithAuthorAndSoftDelete extends UserWithAuthor
{
use \Winter\Storm\Database\Traits\SoftDelete;
}
class UserWithSoftAuthorAndSoftDelete extends UserWithSoftAuthor
{
use \Winter\Storm\Database\Traits\SoftDelete;
}

View File

@@ -0,0 +1,26 @@
<?php namespace Database\Tester\Updates;
use Schema;
use Winter\Storm\Database\Updates\Migration;
class CreateAuthorsTable extends Migration
{
public function up()
{
Schema::create('database_tester_authors', function ($table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->integer('user_id')->unsigned()->index()->nullable();
$table->integer('country_id')->unsigned()->index()->nullable();
$table->string('name')->nullable();
$table->string('email')->nullable();
$table->softDeletes();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('database_tester_authors');
}
}

View File

@@ -0,0 +1,46 @@
<?php namespace Database\Tester\Updates;
use Schema;
use Winter\Storm\Database\Updates\Migration;
class CreateCategoriesTable extends Migration
{
public function up()
{
Schema::create('database_tester_categories', function ($table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->integer('parent_id')->nullable();
$table->string('name')->nullable();
$table->string('slug')->nullable()->index()->unique();
$table->string('description')->nullable();
$table->integer('company_id')->unsigned()->nullable();
$table->string('language', 3)->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('database_tester_categories_nested', function ($table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->integer('parent_id')->nullable();
$table->integer('nest_left')->nullable();
$table->integer('nest_right')->nullable();
$table->integer('nest_depth')->nullable();
$table->string('name')->nullable();
$table->string('slug')->nullable()->index()->unique();
$table->string('description')->nullable();
$table->integer('company_id')->unsigned()->nullable();
$table->string('language', 3)->nullable();
$table->timestamps();
$table->softDeletes();
});
}
public function down()
{
Schema::dropIfExists('database_tester_categories');
Schema::dropIfExists('database_tester_categories_nested');
}
}

View File

@@ -0,0 +1,23 @@
<?php namespace Database\Tester\Updates;
use Schema;
use Winter\Storm\Database\Updates\Migration;
class CreateCountriesTable extends Migration
{
public function up()
{
Schema::create('database_tester_countries', function ($table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->string('name')->nullable();
$table->softDeletes();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('database_tester_countries');
}
}

View File

@@ -0,0 +1,26 @@
<?php namespace Database\Tester\Updates;
use Schema;
use Winter\Storm\Database\Updates\Migration;
class CreateEventLogTable extends Migration
{
public function up()
{
Schema::create('database_tester_event_log', function ($table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->string('action', 30)->nullable();
$table->string('related_id')->index()->nullable();
$table->string('related_type')->index()->nullable();
$table->softDeletes();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('database_tester_event_log');
}
}

View File

@@ -0,0 +1,30 @@
<?php namespace Database\Tester\Updates;
use Schema;
use Winter\Storm\Database\Updates\Migration;
class CreateMetaTable extends Migration
{
public function up()
{
Schema::create('database_tester_meta', function ($table) {
$table->engine = 'InnoDB';
$table->increments('id')->unsigned();
$table->integer('taggable_id')->unsigned()->index()->nullable();
$table->string('taggable_type')->nullable();
$table->string('meta_title')->nullable();
$table->string('meta_description')->nullable();
$table->string('meta_keywords')->nullable();
$table->string('canonical_url')->nullable();
$table->string('redirect_url')->nullable();
$table->string('robot_index')->nullable();
$table->string('robot_follow')->nullable();
});
}
public function down()
{
Schema::dropIfExists('database_tester_meta');
}
}

View File

@@ -0,0 +1,24 @@
<?php namespace Database\Tester\Updates;
use Schema;
use Winter\Storm\Database\Updates\Migration;
class CreatePhonesTable extends Migration
{
public function up()
{
Schema::create('database_tester_phones', function ($table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->string('number')->nullable();
$table->integer('author_id')->unsigned()->index()->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('database_tester_phones');
}
}

View File

@@ -0,0 +1,41 @@
<?php namespace Database\Tester\Updates;
use Schema;
use Winter\Storm\Database\Updates\Migration;
class CreatePostsTable extends Migration
{
public function up()
{
Schema::create('database_tester_posts', function ($table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->string('title')->nullable();
$table->string('slug')->nullable()->index();
$table->text('long_slug')->nullable();
$table->text('description')->nullable();
$table->boolean('is_published')->default(false);
$table->timestamp('published_at')->nullable();
$table->integer('author_id')->unsigned()->index()->nullable();
$table->string('author_nickname')->default('Winter')->nullable();
$table->softDeletes();
$table->timestamps();
});
Schema::create('database_tester_categories_posts', function ($table) {
$table->engine = 'InnoDB';
$table->integer('category_id')->unsigned();
$table->integer('post_id')->unsigned();
$table->primary(['category_id', 'post_id']);
$table->string('category_name')->nullable();
$table->string('post_name')->nullable();
});
}
public function down()
{
Schema::dropIfExists('database_tester_categories_posts');
Schema::dropIfExists('database_tester_posts');
}
}

View File

@@ -0,0 +1,34 @@
<?php namespace Database\Tester\Updates;
use Schema;
use Winter\Storm\Database\Updates\Migration;
class CreateRolesTable extends Migration
{
public function up()
{
Schema::create('database_tester_roles', function ($table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->string('name')->nullable();
$table->text('description')->nullable();
$table->timestamps();
});
Schema::create('database_tester_authors_roles', function ($table) {
$table->engine = 'InnoDB';
$table->integer('author_id')->unsigned();
$table->integer('role_id')->unsigned();
$table->primary(['author_id', 'role_id']);
$table->string('clearance_level')->nullable();
$table->boolean('is_executive')->default(false);
});
}
public function down()
{
Schema::dropIfExists('database_tester_roles');
Schema::dropIfExists('database_tester_authors_roles');
}
}

View File

@@ -0,0 +1,31 @@
<?php namespace Database\Tester\Updates;
use Schema;
use Winter\Storm\Database\Updates\Migration;
class CreateTagsTable extends Migration
{
public function up()
{
Schema::create('database_tester_tags', function ($table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->string('name');
$table->timestamps();
});
Schema::create('database_tester_taggables', function ($table) {
$table->engine = 'InnoDB';
$table->unsignedInteger('tag_id');
$table->morphs('taggable', 'testings_taggable');
$table->unsignedInteger('added_by')->nullable();
});
}
public function down()
{
Schema::dropIfExists('database_tester_taggables');
Schema::dropIfExists('database_tester_tags');
}
}

View File

@@ -0,0 +1,25 @@
<?php namespace Database\Tester\Updates;
use Schema;
use Winter\Storm\Database\Updates\Migration;
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('database_tester_users', function ($table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->string('name')->nullable();
$table->string('email')->nullable();
$table->softDeletes();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('database_tester_users');
}
}

View File

@@ -0,0 +1,13 @@
1.0.1: First version of Tester
1.0.2:
- Create tables
- create_posts_table.php
- create_authors_table.php
- create_phones_table.php
- create_categories_table.php
- create_roles_table.php
- create_users_table.php
- create_event_log_table.php
- create_meta_table.php
- create_countries_table.php
- create_tags_table.php

View File

@@ -0,0 +1,28 @@
<?php namespace DependencyTest\Acme;
use Backend;
use Backend\Models\UserRole;
use System\Classes\PluginBase;
/**
* Acme Plugin Information File
*/
class Plugin extends PluginBase
{
public $require = [
'DependencyTest.Dependency',
];
/**
* Returns information about this plugin.
*/
public function pluginDetails(): array
{
return [
'name' => 'ACME',
'description' => 'This is a test plugin that will be used to check dependencies are loaded first.',
'author' => 'Eric Pfeiffer',
'icon' => 'icon-leaf'
];
}
}

View File

@@ -0,0 +1,2 @@
'1.0.0':
- 'First version of Acme'

View File

@@ -0,0 +1,16 @@
<?php namespace DependencyTest\Dependency;
use System\Classes\PluginBase;
class Plugin extends PluginBase
{
public function pluginDetails()
{
return [
'name' => 'Dependency Test - Dependency',
'description' => 'This is a test plugin that will act as a dependency for the other test plugins in this
namespace.',
'author' => 'Ben Thomson'
];
}
}

View File

@@ -0,0 +1 @@
1.0.1: Initial version of the plugin

View File

@@ -0,0 +1,19 @@
<?php namespace DependencyTest\Found;
use System\Classes\PluginBase;
class Plugin extends PluginBase
{
public $require = [
'DependencyTest.Dependency'
];
public function pluginDetails()
{
return [
'name' => 'Dependency Test - Found',
'description' => 'This is a test plugin with a dependency that exists.',
'author' => 'Ben Thomson'
];
}
}

View File

@@ -0,0 +1 @@
1.0.1: Initial version of the plugin

View File

@@ -0,0 +1,19 @@
<?php namespace DependencyTest\NotFound;
use System\Classes\PluginBase;
class Plugin extends PluginBase
{
public $require = [
'DependencyTest.Missing'
];
public function pluginDetails()
{
return [
'name' => 'Dependency Test - Not Found',
'description' => 'This is a test plugin with a dependency that does not exist.',
'author' => 'Ben Thomson'
];
}
}

View File

@@ -0,0 +1 @@
1.0.1: Initial version of the plugin

View File

@@ -0,0 +1,19 @@
<?php namespace DependencyTest\WrongCase;
use System\Classes\PluginBase;
class Plugin extends PluginBase
{
public $require = [
'Dependencytest.dependency'
];
public function pluginDetails()
{
return [
'name' => 'Dependency Test - Wrong Case',
'description' => 'This is a test plugin with a dependency that exists, but is using the wrong letter case.',
'author' => 'Ben Thomson'
];
}
}

View File

@@ -0,0 +1 @@
1.0.1: Initial version of the plugin

Some files were not shown because too many files have changed in this diff Show More