├── .gitignore ├── LICENSE ├── Plugin.php ├── README.md ├── classes ├── DatabaseSnapshot.php └── ReloadProvidersMiddleware.php ├── composer.json ├── dusk └── Browser.php ├── factories ├── BackendUserFactory.php └── RainLabUserFactory.php ├── http └── controllers │ └── DuskUserController.php ├── phpunit.xml.dist ├── tests ├── CreatesApplication.php ├── DuskTestCase.php ├── TestCase.php ├── bootstrap.php ├── browser │ ├── ExampleTest.php │ ├── components │ │ └── Example.php │ ├── console │ │ └── .gitignore │ ├── pages │ │ ├── HomePage.php │ │ └── Page.php │ └── screenshots │ │ └── .gitignore ├── feature │ └── ExampleTest.php └── unit │ └── ExampleTest.php └── updates └── version.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | phpunit.xml 3 | tests/temp 4 | vendor 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 inetis.ch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Plugin.php: -------------------------------------------------------------------------------- 1 | 'Testing', 24 | 'description' => 'Add dependecies for run Laravel Dusk on OctoberCMS', 25 | 'author' => 'inetis', 26 | 'icon' => 'icon-code', 27 | ]; 28 | } 29 | 30 | public function boot() 31 | { 32 | if (!App::environment('dusk')) { 33 | return; 34 | } 35 | 36 | Route::get('/_dusk/login/{userId}/{provider}', [ 37 | 'middleware' => 'web', 38 | 'uses' => DuskUserController::class . '@login', 39 | ]); 40 | 41 | Route::get('/_dusk/logout/{provider?}', [ 42 | 'middleware' => 'web', 43 | 'uses' => DuskUserController::class . '@logout', 44 | ]); 45 | 46 | Route::get('/_dusk/user/{provider}', [ 47 | 'middleware' => 'web', 48 | 'uses' => DuskUserController::class . '@user', 49 | ]); 50 | } 51 | 52 | public function register() 53 | { 54 | $this->app->singleton(EloquentFactory::class, function ($app) { 55 | return EloquentFactory::construct( 56 | $app->make(FakerGenerator::class), plugins_path('inetis/testing/factories') 57 | ); 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Test for OctoberCMS site 2 | 3 | An easy way to use the power of Laravel testing inside your OctoberCMS site. 4 | 5 | - Browser testing with Laravel Dusk 6 | example in `tests/browser/ExampleTest.php` 7 | documentation: https://laravel.com/docs/5.5/dusk 8 | 9 | - HTTP request testing 10 | example in `tests/feature/ExampleTest.php` 11 | documentation: https://laravel.com/docs/5.5/http-tests 12 | 13 | - Unit testing 14 | example in `tests/unit/ExampleTest.php` 15 | 16 | ## Setup 17 | 1. Copy content of this repos in `plugins/inetis/testing` 18 | 2. Go to `plugins/inetis/testing` and run `composer install` 19 | 3. Run tests `composer test` 20 | 21 | ## Informations 22 | There is default [laravel factories](https://laravel.com/docs/5.5/database-testing#writing-factories) for Backend Users 23 | and RainLab Users in `factories` directory. You can adapt them or create new ones. 24 | 25 | By default all is configured for the changes made during tests are not persistantes. For this work with Dusk test (that 26 | perform real HTTP request) a dump of the database is performed before tests and restored after. -------------------------------------------------------------------------------- /classes/DatabaseSnapshot.php: -------------------------------------------------------------------------------- 1 | snapshotsDirectory = plugins_path('inetis/testing/tests/temp/snapshots/database/'); 18 | } 19 | 20 | public function dump($force = false) 21 | { 22 | if (!empty(self::$snapshot) && !$force) { 23 | return; 24 | } 25 | 26 | if (!file_exists($directory = $this->snapshotsDirectory)) { 27 | mkdir($directory, 0777, true); 28 | } 29 | 30 | DbDumperFactory::createForConnection(config('database.default'))->dumpToFile( 31 | self::$snapshot = $directory . now()->format('Ymd-H.i.s.u') . '.sql' 32 | ); 33 | } 34 | 35 | public function restore() 36 | { 37 | $tableDropper = TableDropperFactory::create(DB::getDriverName()); 38 | $tableDropper->dropAllTables(); 39 | 40 | DB::reconnect(); 41 | 42 | DB::connection(config('database.default'))->unprepared( 43 | file_get_contents(self::$snapshot) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /classes/ReloadProvidersMiddleware.php: -------------------------------------------------------------------------------- 1 | executionContextProvider = app()->getProvider(ExecutionContextProvider::class); 35 | $this->systemServiceProvider = app()->getProvider(SystemServiceProvider::class); 36 | $this->cmsServiceProvider = app()->getProvider(CmsServiceProvider::class); 37 | $this->backendServiceProvider = app()->getProvider(BackendServiceProvider::class); 38 | } 39 | 40 | /** 41 | * Handle an incoming request. 42 | * 43 | * @param \Illuminate\Http\Request $request 44 | * @param Closure $next 45 | * 46 | * @return mixed 47 | */ 48 | public function handle($request, Closure $next) 49 | { 50 | $this->executionContextProvider->register(); 51 | 52 | // @todo uncomment after OC 460 release (@see https://github.com/octobercms/october/pull/4751) 53 | // $this->systemServiceProvider->register(); 54 | 55 | $this->cmsServiceProvider->register(); 56 | $this->backendServiceProvider->register(); 57 | 58 | return $next($request); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "fzaninotto/faker": "~1.7", 4 | "laravel/dusk": "^2.0", 5 | "mockery/mockery": "~1.0", 6 | "phpunit/phpunit": "~6.5", 7 | "spatie/laravel-db-snapshots": "^1.2" 8 | }, 9 | "scripts": { 10 | "test": "phpunit" 11 | }, 12 | "replace": { 13 | "laravel/framework": "5.5.*", 14 | "illuminate/auth": "5.5.*", 15 | "illuminate/broadcasting": "5.5.*", 16 | "illuminate/bus": "5.5.*", 17 | "illuminate/cache": "5.5.*", 18 | "illuminate/config": "5.5.*", 19 | "illuminate/console": "5.5.*", 20 | "illuminate/container": "5.5.*", 21 | "illuminate/contracts": "5.5.*", 22 | "illuminate/cookie": "5.5.*", 23 | "illuminate/database": "5.5.*", 24 | "illuminate/encryption": "5.5.*", 25 | "illuminate/events": "5.5.*", 26 | "illuminate/filesystem": "5.5.*", 27 | "illuminate/hashing": "5.5.*", 28 | "illuminate/http": "5.5.*", 29 | "illuminate/log": "5.5.*", 30 | "illuminate/mail": "5.5.*", 31 | "illuminate/notifications": "5.5.*", 32 | "illuminate/pagination": "5.5.*", 33 | "illuminate/pipeline": "5.5.*", 34 | "illuminate/queue": "5.5.*", 35 | "illuminate/redis": "5.5.*", 36 | "illuminate/routing": "5.5.*", 37 | "illuminate/session": "5.5.*", 38 | "illuminate/support": "5.5.*", 39 | "illuminate/translation": "5.5.*", 40 | "illuminate/validation": "5.5.*", 41 | "illuminate/view": "5.5.*" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /dusk/Browser.php: -------------------------------------------------------------------------------- 1 | getKey() : $userId; 28 | 29 | return $this->visit(rtrim('/_dusk/login/' . $userId . '/' . $provider, '/')); 30 | } 31 | 32 | 33 | /** 34 | * Wait for the AJAX request end 35 | * 36 | * @param int $seconds 37 | * @return $this 38 | */ 39 | public function waitForAjax($seconds = null) 40 | { 41 | $this->waitFor('.stripe-loading-indicator.loaded', $seconds); 42 | 43 | return $this; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /factories/BackendUserFactory.php: -------------------------------------------------------------------------------- 1 | define(User::class, function (Faker\Generator $faker) { 9 | return [ 10 | 'first_name' => $faker->firstName, 11 | 'last_name' => $faker->lastName, 12 | 'login' => $faker->userName, 13 | 'email' => $faker->safeEmail, 14 | 'password' => $password = $faker->password(8), 15 | 'password_confirmation' => $password, 16 | 'is_activated' => true, 17 | 'activated_at' => now(), 18 | 'role_id' => null, 19 | 'is_superuser' => false, 20 | ]; 21 | }); 22 | 23 | $factory->state(User::class, 'superuser', [ 24 | 'is_superuser' => true, 25 | ]); 26 | 27 | $factory->state(User::class, 'role:publisher', function () { 28 | $role = UserRole::where('code', 'publisher')->first(); 29 | 30 | return ['role_id' => $role->id ?? null]; 31 | }); 32 | 33 | $factory->state(User::class, 'role:developer', function () { 34 | $role = UserRole::where('code', 'developer')->first(); 35 | 36 | return ['role_id' => $role->id ?? null]; 37 | }); 38 | -------------------------------------------------------------------------------- /factories/RainLabUserFactory.php: -------------------------------------------------------------------------------- 1 | define(\RainLab\User\Models\User::class, function (Faker\Generator $faker) { 5 | return [ 6 | 'name' => $faker->name, 7 | 'surname' => $faker->lastName, 8 | 'email' => $email = $faker->safeEmail, 9 | 'password' => $password = $faker->password(8), 10 | 'password_confirmation' => $password, 11 | 'is_activated' => true, 12 | 'activated_at' => now(), 13 | 'username' => $email, 14 | 'is_guest' => false, 15 | 'is_superuser' => false, 16 | ]; 17 | }); 18 | -------------------------------------------------------------------------------- /http/controllers/DuskUserController.php: -------------------------------------------------------------------------------- 1 | getAuthManagerForProvider($provider)->getUser(); 17 | 18 | if (!$user) { 19 | return []; 20 | } 21 | 22 | return [ 23 | 'id' => $user->getAuthIdentifier(), 24 | 'className' => get_class($user), 25 | ]; 26 | } 27 | 28 | /** 29 | * Login using the given user ID / email. 30 | * 31 | * @param string $userId 32 | * @param string $provider 33 | * 34 | * @throws \October\Rain\Auth\AuthException 35 | */ 36 | public function login($userId, $provider) 37 | { 38 | $model = $this->modelForProvider($provider); 39 | 40 | if (str_contains($userId, '@')) { 41 | $user = (new $model)->where('email', $userId)->first(); 42 | } else { 43 | $user = (new $model)->find($userId); 44 | } 45 | 46 | $this->getAuthManagerForProvider($provider)->login($user); 47 | } 48 | 49 | /** 50 | * Log the user out of the application. 51 | * 52 | * @param string $guard 53 | */ 54 | public function logout($provider = null) 55 | { 56 | if ($provider) { 57 | $this->getAuthManagerForProvider($provider)->logout(); 58 | 59 | return; 60 | } 61 | 62 | app('backend.auth')->logout(); 63 | 64 | if (class_exists(\RainLab\User\Models\User::class)) { 65 | app('user.auth')->logout(); 66 | } 67 | } 68 | 69 | /** 70 | * Get the model for the given provider. 71 | * 72 | * @param string $provider 73 | * 74 | * @return string 75 | */ 76 | protected function modelForProvider($provider) 77 | { 78 | switch ($provider) { 79 | case 'rainlab.user': 80 | case 'frontend': 81 | return \RainLab\User\Models\User::class; 82 | case 'backend': 83 | return \Backend\Models\User::class; 84 | } 85 | } 86 | 87 | /** 88 | * @param $provider 89 | * 90 | * @return \October\Rain\Auth\Manager 91 | */ 92 | protected function getAuthManagerForProvider($provider) 93 | { 94 | switch ($provider) { 95 | case 'rainlab.user': 96 | case 'frontend': 97 | return app('user.auth'); 98 | case 'backend': 99 | return app('backend.auth'); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/DuskTestCase.php: -------------------------------------------------------------------------------- 1 | databaseSnapshot = new DatabaseSnapshot; 29 | $this->databaseSnapshot->dump(); 30 | 31 | Browser::$storeScreenshotsAt = __DIR__ . '/browser/screenshots'; 32 | Browser::$storeConsoleLogAt = __DIR__ . '/browser/console'; 33 | 34 | $this->setDuskEnv(); 35 | } 36 | 37 | public function tearDown() 38 | { 39 | $this->databaseSnapshot->restore(); 40 | 41 | $this->restoreEnv(); 42 | 43 | parent::tearDown(); 44 | } 45 | 46 | /** 47 | * Prepare for Dusk test execution. 48 | * 49 | * @beforeClass 50 | * @return void 51 | */ 52 | public static function prepare() 53 | { 54 | static::startChromeDriver(); 55 | } 56 | 57 | /** 58 | * Create the RemoteWebDriver instance. 59 | * 60 | * @return \Facebook\WebDriver\Remote\RemoteWebDriver 61 | */ 62 | protected function driver() 63 | { 64 | $options = (new ChromeOptions)->addArguments([ 65 | '--disable-gpu', 66 | '--headless', 67 | '--window-size=1920,1080', 68 | ]); 69 | 70 | return RemoteWebDriver::create( 71 | 'http://localhost:9515', 72 | DesiredCapabilities::chrome()->setCapability( 73 | ChromeOptions::CAPABILITY, $options 74 | ) 75 | ); 76 | } 77 | 78 | /** 79 | * Create a new Browser instance. 80 | * 81 | * @param \Facebook\WebDriver\Remote\RemoteWebDriver $driver 82 | * 83 | * @return \Laravel\Dusk\Browser 84 | */ 85 | protected function newBrowser($driver) 86 | { 87 | return new Browser($driver); 88 | } 89 | 90 | private function setDuskEnv() 91 | { 92 | $this->envFile = base_path('.env'); 93 | 94 | if (file_exists($this->envFile)) { 95 | $this->originalEnvFile = base_path('.env.' . str_replace('.', '', microtime(true))); 96 | 97 | rename($this->envFile, $this->originalEnvFile); 98 | } 99 | 100 | file_put_contents($this->envFile, 'APP_ENV=dusk'); 101 | } 102 | 103 | private function restoreEnv() 104 | { 105 | unlink($this->envFile); 106 | 107 | if (!empty($this->originalEnvFile)) { 108 | rename($this->originalEnvFile, $this->envFile); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | app->make(Kernel::class)->prependMiddleware( 21 | ReloadProvidersMiddleware::class 22 | ); 23 | } 24 | 25 | /** 26 | * Set the currently logged in user for the application. 27 | * 28 | * @param \Illuminate\Contracts\Auth\Authenticatable $user 29 | * @param string|null $driver 30 | * 31 | * @return void 32 | */ 33 | public function be(UserContract $user, $driver = null) 34 | { 35 | if ($user instanceof \RainLab\User\Models\User) { 36 | $this->app['user.auth']->setUser($user); 37 | } 38 | 39 | if ($user instanceof \Backend\Models\User) { 40 | $this->app['backend.auth']->setUser($user); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | register(); 20 | $loader->addDirectories([ 21 | 'modules', 22 | 'plugins', 23 | ]); 24 | 25 | /* 26 | * Plugin autoloader 27 | */ 28 | require __DIR__ . '/../vendor/autoload.php'; 29 | -------------------------------------------------------------------------------- /tests/browser/ExampleTest.php: -------------------------------------------------------------------------------- 1 | browse(function (Browser $browser) { 17 | $browser->visit('/') 18 | ->assertSee('The demo'); 19 | }); 20 | } 21 | 22 | /** @test */ 23 | public function page_not_found() 24 | { 25 | $this->browse(function (Browser $browser) { 26 | $browser->visit('/404') 27 | ->assertSee('Page not found'); 28 | }); 29 | } 30 | 31 | /** @test */ 32 | public function railab_authentication() 33 | { 34 | $user = factory(RainLabUser::class)->create(); 35 | 36 | $this->browse(function (Browser $browser) { 37 | $browser->visit('/account') 38 | ->assertPathIs('/login'); 39 | }); 40 | 41 | $this->browse(function (Browser $browser) use ($user) { 42 | $browser 43 | ->loginAs($user) 44 | ->visit('/account') 45 | ->assertSee('My Account') 46 | ->assertValue('input[name="email"]', $user->email) 47 | ->type('name', 'Test User UPDATED') 48 | ->press('Save') 49 | ->waitForReload() 50 | ->assertValue('input[name="name"]', 'Test User UPDATED') 51 | ->screenshot('updateuser') 52 | ->logout(); 53 | }); 54 | 55 | $this->assertEquals('Test User UPDATED', $user->reload()->name); 56 | } 57 | 58 | /** @test */ 59 | public function backend_authentication() 60 | { 61 | $user = factory(BackendUser::class) 62 | ->states('role:publisher') 63 | ->create(); 64 | 65 | $this->browse(function (Browser $browser) { 66 | $browser->visit(Backend::uri()) 67 | ->assertPathIs(Backend::uri() . '/backend/auth/signin'); 68 | }); 69 | 70 | $this->browse(function (Browser $browser) use ($user) { 71 | $browser 72 | ->loginAs($user) 73 | ->visit(Backend::uri()) 74 | ->assertSee('Settings') 75 | ->click('#layout-mainmenu li.mainmenu-account > a') 76 | ->clickLink('My account') 77 | ->assertValue('input[name="User[email]"]', $user->email) 78 | ->logout(); 79 | }); 80 | } 81 | 82 | /** @test */ 83 | public function frontend_ajax_calculator() 84 | { 85 | $this->browse(function (Browser $browser) { 86 | $browser->visit('/') 87 | ->clickLink('AJAX framework') 88 | ->assertPathIs('/demo/ajax') 89 | ->type('value1', '50') 90 | ->select('operation', '-') 91 | ->type('value2', '10') 92 | ->press('Calculate') 93 | ->waitForAjax() 94 | ->assertSeeIn('#result', '40'); 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/browser/components/Example.php: -------------------------------------------------------------------------------- 1 | assertVisible($this->selector()); 29 | } 30 | 31 | /** 32 | * Get the element shortcuts for the component. 33 | * 34 | * @return array 35 | */ 36 | public function elements() 37 | { 38 | return [ 39 | '@element' => '#selector', 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/browser/console/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/browser/pages/HomePage.php: -------------------------------------------------------------------------------- 1 | assertPathIs($this->url()); 28 | } 29 | 30 | /** 31 | * Get the element shortcuts for the page. 32 | * 33 | * @return array 34 | */ 35 | public function elements() 36 | { 37 | return [ 38 | '@element' => '#selector', 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/browser/pages/Page.php: -------------------------------------------------------------------------------- 1 | '#selector', 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/browser/screenshots/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/') 16 | ->assertSeeText('The demo TEST') 17 | ->assertStatus(200); 18 | } 19 | 20 | /** @test */ 21 | public function frontend_auth() 22 | { 23 | $user = factory(RainLabUser::class)->create(); 24 | 25 | $this->get('/account') 26 | ->assertRedirect('/login'); 27 | 28 | $this->actingAs($user) 29 | ->get('/account') 30 | ->assertStatus(200); 31 | } 32 | 33 | /** @test */ 34 | public function backend_auth() 35 | { 36 | $user = factory(BackendUser::class) 37 | ->states('role:developer') 38 | ->create(); 39 | 40 | $this->get(Backend::url()) 41 | ->assertRedirect(Backend::url('backend/auth')); 42 | 43 | $this->actingAs($user) 44 | ->followingRedirects() 45 | ->get(Backend::url('backend')) 46 | ->assertStatus(200) 47 | ->assertSee('Sign out'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/unit/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /updates/version.yaml: -------------------------------------------------------------------------------- 1 | 1.0.0: First version of Testing 2 | --------------------------------------------------------------------------------